日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
再見Session!這個(gè)跨域認(rèn)證解決方案真的優(yōu)雅!
  • 客戶端向服務(wù)器端發(fā)送用戶名和密碼
  • 服務(wù)器端驗(yàn)證通過后,在當(dāng)前會(huì)話(session)中保存相關(guān)數(shù)據(jù),比如說登錄時(shí)間、登錄 IP 等。
  • 服務(wù)器端向客戶端返回一個(gè) session_id,客戶端將其保存在 Cookie 中。
  • 客戶端再向服務(wù)器端發(fā)起請(qǐng)求時(shí),將 session_id 傳回給服務(wù)器端。
  • 服務(wù)器端拿到 session_id 后,對(duì)用戶的身份進(jìn)行鑒定。

單機(jī)情況下,這種模式是沒有任何問題的,但對(duì)于前后端分離的 Web 應(yīng)用來說,就非常痛苦了。于是就有了另外一種解決方案,服務(wù)器端不再保存 session 數(shù)據(jù),而是將其保存在客戶端,客戶端每次發(fā)起請(qǐng)求時(shí)再把這個(gè)數(shù)據(jù)發(fā)送給服務(wù)器端進(jìn)行驗(yàn)證。JWT(JSON Web Token)就是這種方案的典型代表。

為婁底等地區(qū)用戶提供了全套網(wǎng)頁(yè)設(shè)計(jì)制作服務(wù),及婁底網(wǎng)站建設(shè)行業(yè)解決方案。主營(yíng)業(yè)務(wù)為成都做網(wǎng)站、網(wǎng)站建設(shè)、婁底網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!

一、關(guān)于 JWT

JWT,是目前最流行的一個(gè)跨域認(rèn)證解決方案:客戶端發(fā)起用戶登錄請(qǐng)求,服務(wù)器端接收并認(rèn)證成功后,生成一個(gè) JSON 對(duì)象(如下所示),然后將其返回給客戶端。

{
"sub": "wanger",
"created": 1645700436900,
"exp": 1646305236
}

客戶端再次與服務(wù)器端通信的時(shí)候,把這個(gè) JSON 對(duì)象捎帶上,作為前后端互相信任的一個(gè)憑證。服務(wù)器端接收到請(qǐng)求后,通過 JSON 對(duì)象對(duì)用戶身份進(jìn)行鑒定,這樣就不再需要保存任何 session 數(shù)據(jù)了。

假如我現(xiàn)在使用用戶名 wanger 和密碼 123456 進(jìn)行訪問編程喵(Codingmore)的 login 接口,那么實(shí)際的 JWT 是一串看起來像是加過密的字符串。

為了讓大家看的更清楚一點(diǎn),我將其復(fù)制到了 jwt 的官網(wǎng)。

左側(cè) Encoded 部分就是 JWT 密文,中間用「.」分割成了三部分(右側(cè) Decoded 部分):

  • Header(頭部),描述 JWT 的元數(shù)據(jù),其中 alg 屬性表示簽名的算法(當(dāng)前為 HS512);
  • Payload(負(fù)載),用來存放實(shí)際需要傳遞的數(shù)據(jù),其中 sub 屬性表示主題(實(shí)際值為用戶名),created 屬性表示 JWT 產(chǎn)生的時(shí)間,exp 屬性表示過期時(shí)間
  • Signature(簽名),對(duì)前兩部分的簽名,防止數(shù)據(jù)篡改;這里需要服務(wù)器端指定一個(gè)密鑰(只有服務(wù)器端才知道),不能泄露給客戶端,然后使用 Header 中指定的簽名算法,按照下面的公式產(chǎn)生簽名:
HMACSHA512(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)

算出簽名后,再把 Header、Payload、Signature 拼接成一個(gè)字符串,中間用「.」分割,就可以返回給客戶端了。

客戶端拿到 JWT 后,可以放在 localStorage,也可以放在 Cookie 里面。

const TokenKey = '1D596CD8-8A20-4CEC-98DD-CDC12282D65C' // createUuid()

export function getToken () {
return Cookies.get(TokenKey)
}

export function setToken (token) {
return Cookies.set(TokenKey, token)
}

以后客戶端再與服務(wù)器端通信的時(shí)候,就帶上這個(gè) JWT,一般放在 HTTP 的請(qǐng)求的頭信息 Authorization 字段里。

Authorization: Bearer 

服務(wù)器端接收到請(qǐng)求后,再對(duì) JWT 進(jìn)行驗(yàn)證,如果驗(yàn)證通過就返回相應(yīng)的資源。

二、實(shí)戰(zhàn) JWT

第一步,在 pom.xml 文件中添加 JWT 的依賴。


io.jsonwebtoken
jjwt
0.9.0

第二步,在 application.yml 中添加 JWT 的配置項(xiàng)。

jwt:
tokenHeader: Authorization #JWT存儲(chǔ)的請(qǐng)求頭
secret: codingmore-admin-secret #JWT加解密使用的密鑰
expiration: 604800 #JWT的超期限時(shí)間(60*60*24*7)
tokenHead: 'Bearer ' #JWT負(fù)載中拿到開頭

第三步,新建 JwtTokenUtil.java 工具類,主要有三個(gè)方法:

  • generateToken(UserDetails userDetails):根據(jù)登錄用戶生成 token
  • getUserNameFromToken(String token):從 token 中獲取登錄用戶
  • validateToken(String token, UserDetails userDetails):判斷 token 是否仍然有效
public class JwtTokenUtil {

@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;

/**
* 根據(jù)用戶信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}

/**
* 根據(jù)用戶名、創(chuàng)建時(shí)間生成JWT的token
*/
private String generateToken(Map claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

/**
* 從token中獲取登錄用戶名
*/
public String getUserNameFromToken(String token) {
String username = null;
Claims claims = getClaimsFromToken(token);
if (claims != null) {
username = claims.getSubject();
}

return username;
}

/**
* 從token中獲取JWT中的負(fù)載
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式驗(yàn)證失敗:{}", token);
}
return claims;
}

/**
* 驗(yàn)證token是否還有效
*
* @param token 客戶端傳入的token
* @param userDetails 從數(shù)據(jù)庫(kù)中查詢出來的用戶信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

/**
* 判斷token是否已經(jīng)失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}

/**
* 從token中獲取過期時(shí)間
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
}

第四步, 在 UsersController.java 中新增 login 登錄接口,接收用戶名和密碼,并將 JWT 返回給客戶端。

@Controller
@Api(tags="用戶")
@RequestMapping("/users")
public class UsersController {
@Autowired
private IUsersService usersService;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;

@ApiOperation(value = "登錄以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
String token = usersService.login(users.getUserLogin(), users.getUserPass());

if (token == null) {
return ResultObject.validateFailed("用戶名或密碼錯(cuò)誤");
}

// 將 JWT 傳遞回客戶端
Map tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}

}

第五步,在 UsersServiceImpl.java 中新增 login 方法,根據(jù)用戶名從數(shù)據(jù)庫(kù)中查詢用戶,密碼驗(yàn)證通過后生成 JWT。

@Service
public class UsersServiceImpl extends ServiceImpl implements IUsersService {

@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;

public String login(String username, String password) {
String token = null;
//密碼需要客戶端加密后傳遞
try {
// 查詢用戶+用戶資源
UserDetails userDetails = loadUserByUsername(username);

// 驗(yàn)證密碼
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
Asserts.fail("密碼不正確");
}

// 返回 JWT
token = jwtTokenUtil.generateToken(userDetails);
} catch (AuthenticationException e) {
LOGGER.warn("登錄異常:{}", e.getMessage());
}
return token;
}
}

第六步,新增 JwtAuthenticationTokenFilter.java,每次客戶端發(fā)起請(qǐng)求時(shí)對(duì) JWT 進(jìn)行驗(yàn)證。

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 從客戶端請(qǐng)求中獲取 JWT
String authHeader = request.getHeader(this.tokenHeader);
// 該 JWT 是我們規(guī)定的格式,以 tokenHead 開頭
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
// The part after "Bearer "
String authToken = authHeader.substring(this.tokenHead.length());
// 從 JWT 中獲取用戶名
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);

// SecurityContextHolder 是 SpringSecurity 的一個(gè)工具類
// 保存應(yīng)用程序中當(dāng)前使用人的安全上下文
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 根據(jù)用戶名獲取登錄用戶信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 驗(yàn)證 token 是否過期
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 將登錄用戶保存到安全上下文中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);

LOGGER.info("authenticated user:{}", username);
}
}
}
chain.doFilter(request, response);
}
}

JwtAuthenticationTokenFilter 繼承了 OncePerRequestFilter,該過濾器能確保一次請(qǐng)求只通過一次 filter,而不需要重復(fù)執(zhí)行。也就是說,客戶端每發(fā)起一次請(qǐng)求,該過濾器就會(huì)執(zhí)行一次。

這個(gè)過濾器非常關(guān)鍵啊,基本上每行代碼我都添加了注釋,當(dāng)然了,為了確保大家都能搞清楚這個(gè)類到底做了什么,我再來畫一幅流程圖,這樣就一清二楚了。

SpringSecurity 是一個(gè)安全管理框架,可以和 Spring Boot 應(yīng)用無縫銜接,SecurityContextHolder 是其中非常關(guān)鍵的一個(gè)工具類,持有安全上下文信息,里面保存有當(dāng)前操作的用戶是誰,用戶是否已經(jīng)被認(rèn)證,用戶擁有的權(quán)限等關(guān)鍵信息。

SecurityContextHolder 默認(rèn)使用了 ThreadLocal 策略來存儲(chǔ)認(rèn)證信息,ThreadLocal 的特點(diǎn)是存在它里邊的數(shù)據(jù),哪個(gè)線程存的,哪個(gè)線程才能訪問到。這就意味著不同的請(qǐng)求進(jìn)入到服務(wù)器端后,會(huì)由不同的 Thread 去處理,例如線程 A 將請(qǐng)求 1 的用戶信息存入了 ThreadLocal,線程 B 在處理請(qǐng)求 2 的時(shí)候是無法獲取到用戶信息的。

所以說 JwtAuthenticationTokenFilter 過濾器會(huì)在每次請(qǐng)求過來的時(shí)候進(jìn)行一遍 JWT 的驗(yàn)證,確??蛻舳诉^來的請(qǐng)求是安全的。然后 SpringSecurity 才會(huì)對(duì)接下來的請(qǐng)求接口放行。這也是 JWT 和 Session 的根本區(qū)別:

  • JWT 需要每次請(qǐng)求的時(shí)候驗(yàn)證一次,并且只要 JWT 沒有過期,哪怕服務(wù)器端重啟了,認(rèn)證仍然有效。
  • Session 在沒有過期的情況下是不需要重新對(duì)用戶信息進(jìn)行驗(yàn)證的,當(dāng)服務(wù)器端重啟后,用戶需要重新登錄獲取新的 Session。

也就是說,在 JWT 的方案下,服務(wù)器端保存的密鑰(secret)一定不能泄露,否則客戶端就可以根據(jù)簽名算法偽造用戶的認(rèn)證信息了。

三、Swagger 中添加 JWT 驗(yàn)證

第一步,訪問 login 接口,輸入用戶名和密碼進(jìn)行登錄,獲取服務(wù)器端返回的 JWT。

第二步,收集服務(wù)器端返回的 tokenHead 和 token,將其填入 Authorize(注意 tokenHead 和 token 之間有一個(gè)空格)完成登錄認(rèn)證。

第三步,再次請(qǐng)求其他接口時(shí),Swagger 會(huì)自動(dòng)將 Authorization 作為請(qǐng)求頭信息發(fā)送到服務(wù)器端。

第四步,服務(wù)器端接收到該請(qǐng)求后,會(huì)通過 JwtAuthenticationTokenFilter 過濾器對(duì) JWT 進(jìn)行校驗(yàn)。

到此為止,整個(gè)流程全部打通了,完美!

四、總結(jié)

綜上來看,用 JWT 來解決前后端分離項(xiàng)目中的跨域認(rèn)證還是非常絲滑的,這主要得益于 JSON 的通用性,可以跨語(yǔ)言,JavaScript 和 Java 都支持;另外,JWT 的組成非常簡(jiǎn)單,非常便于傳輸;還有 JWT 不需要在服務(wù)器端保存會(huì)話信息(Session),非常易于擴(kuò)展。

當(dāng)然了,為了保證 JWT 的安全性,不要在 JWT 中保存敏感信息,因?yàn)橐坏┧借€泄露,JWT 是很容易在客戶端被解密的;如果可以,請(qǐng)使用 HTTPS 協(xié)議。


標(biāo)題名稱:再見Session!這個(gè)跨域認(rèn)證解決方案真的優(yōu)雅!
網(wǎng)頁(yè)鏈接:http://m.5511xx.com/article/cdcsdoo.html