新聞中心
- 客戶(hù)端向服務(wù)器端發(fā)送用戶(hù)名和密碼
- 服務(wù)器端驗(yàn)證通過(guò)后,在當(dāng)前會(huì)話(huà)(session)中保存相關(guān)數(shù)據(jù),比如說(shuō)登錄時(shí)間、登錄 IP 等。
- 服務(wù)器端向客戶(hù)端返回一個(gè) session_id,客戶(hù)端將其保存在 Cookie 中。
- 客戶(hù)端再向服務(wù)器端發(fā)起請(qǐng)求時(shí),將 session_id 傳回給服務(wù)器端。
- 服務(wù)器端拿到 session_id 后,對(duì)用戶(hù)的身份進(jìn)行鑒定。
單機(jī)情況下,這種模式是沒(méi)有任何問(wèn)題的,但對(duì)于前后端分離的 Web 應(yīng)用來(lái)說(shuō),就非常痛苦了。于是就有了另外一種解決方案,服務(wù)器端不再保存 session 數(shù)據(jù),而是將其保存在客戶(hù)端,客戶(hù)端每次發(fā)起請(qǐng)求時(shí)再把這個(gè)數(shù)據(jù)發(fā)送給服務(wù)器端進(jìn)行驗(yàn)證。JWT(JSON Web Token)就是這種方案的典型代表。

為婁底等地區(qū)用戶(hù)提供了全套網(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ù),秉承以專(zhuān)業(yè)、用心的態(tài)度為用戶(hù)提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶(hù)的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!
一、關(guān)于 JWT
JWT,是目前最流行的一個(gè)跨域認(rèn)證解決方案:客戶(hù)端發(fā)起用戶(hù)登錄請(qǐng)求,服務(wù)器端接收并認(rèn)證成功后,生成一個(gè) JSON 對(duì)象(如下所示),然后將其返回給客戶(hù)端。
{
"sub": "wanger",
"created": 1645700436900,
"exp": 1646305236
}
客戶(hù)端再次與服務(wù)器端通信的時(shí)候,把這個(gè) JSON 對(duì)象捎帶上,作為前后端互相信任的一個(gè)憑證。服務(wù)器端接收到請(qǐng)求后,通過(guò) JSON 對(duì)象對(duì)用戶(hù)身份進(jìn)行鑒定,這樣就不再需要保存任何 session 數(shù)據(jù)了。
假如我現(xiàn)在使用用戶(hù)名 wanger 和密碼 123456 進(jìn)行訪問(wèn)編程喵(Codingmore)的 login 接口,那么實(shí)際的 JWT 是一串看起來(lái)像是加過(guò)密的字符串。
為了讓大家看的更清楚一點(diǎn),我將其復(fù)制到了 jwt 的官網(wǎng)。
左側(cè) Encoded 部分就是 JWT 密文,中間用「.」分割成了三部分(右側(cè) Decoded 部分):
- Header(頭部),描述 JWT 的元數(shù)據(jù),其中 alg 屬性表示簽名的算法(當(dāng)前為 HS512);
- Payload(負(fù)載),用來(lái)存放實(shí)際需要傳遞的數(shù)據(jù),其中 sub 屬性表示主題(實(shí)際值為用戶(hù)名),created 屬性表示 JWT 產(chǎn)生的時(shí)間,exp 屬性表示過(guò)期時(shí)間
- Signature(簽名),對(duì)前兩部分的簽名,防止數(shù)據(jù)篡改;這里需要服務(wù)器端指定一個(gè)密鑰(只有服務(wù)器端才知道),不能泄露給客戶(hù)端,然后使用 Header 中指定的簽名算法,按照下面的公式產(chǎn)生簽名:
HMACSHA512(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
算出簽名后,再把 Header、Payload、Signature 拼接成一個(gè)字符串,中間用「.」分割,就可以返回給客戶(hù)端了。
客戶(hù)端拿到 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)
}
以后客戶(hù)端再與服務(wù)器端通信的時(shí)候,就帶上這個(gè) JWT,一般放在 HTTP 的請(qǐng)求的頭信息 Authorization 字段里。
Authorization: Bearer
服務(wù)器端接收到請(qǐng)求后,再對(duì) JWT 進(jìn)行驗(yàn)證,如果驗(yàn)證通過(guò)就返回相應(yīng)的資源。
二、實(shí)戰(zhàn) JWT
第一步,在 pom.xml 文件中添加 JWT 的依賴(lài)。
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ù)載中拿到開(kāi)頭
第三步,新建 JwtTokenUtil.java 工具類(lèi),主要有三個(gè)方法:
- generateToken(UserDetails userDetails):根據(jù)登錄用戶(hù)生成 token
- getUserNameFromToken(String token):從 token 中獲取登錄用戶(hù)
- 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ù)用戶(hù)信息生成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ù)用戶(hù)名、創(chuàng)建時(shí)間生成JWT的token
*/
private String generateToken(Map claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 從token中獲取登錄用戶(hù)名
*/
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 客戶(hù)端傳入的token
* @param userDetails 從數(shù)據(jù)庫(kù)中查詢(xún)出來(lái)的用戶(hù)信息
*/
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中獲取過(guò)期時(shí)間
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
}
第四步, 在 UsersController.java 中新增 login 登錄接口,接收用戶(hù)名和密碼,并將 JWT 返回給客戶(hù)端。
@Controller
@Api(tags="用戶(hù)")
@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("用戶(hù)名或密碼錯(cuò)誤");
}
// 將 JWT 傳遞回客戶(hù)端
MaptokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}
}
第五步,在 UsersServiceImpl.java 中新增 login 方法,根據(jù)用戶(hù)名從數(shù)據(jù)庫(kù)中查詢(xún)用戶(hù),密碼驗(yàn)證通過(guò)后生成 JWT。
@Service
public class UsersServiceImpl extends ServiceImplimplements IUsersService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
public String login(String username, String password) {
String token = null;
//密碼需要客戶(hù)端加密后傳遞
try {
// 查詢(xún)用戶(hù)+用戶(hù)資源
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,每次客戶(hù)端發(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 {
// 從客戶(hù)端請(qǐng)求中獲取 JWT
String authHeader = request.getHeader(this.tokenHeader);
// 該 JWT 是我們規(guī)定的格式,以 tokenHead 開(kāi)頭
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
// The part after "Bearer "
String authToken = authHeader.substring(this.tokenHead.length());
// 從 JWT 中獲取用戶(hù)名
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
// SecurityContextHolder 是 SpringSecurity 的一個(gè)工具類(lèi)
// 保存應(yīng)用程序中當(dāng)前使用人的安全上下文
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 根據(jù)用戶(hù)名獲取登錄用戶(hù)信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 驗(yàn)證 token 是否過(guò)期
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 將登錄用戶(hù)保存到安全上下文中
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,該過(guò)濾器能確保一次請(qǐng)求只通過(guò)一次 filter,而不需要重復(fù)執(zhí)行。也就是說(shuō),客戶(hù)端每發(fā)起一次請(qǐng)求,該過(guò)濾器就會(huì)執(zhí)行一次。
這個(gè)過(guò)濾器非常關(guān)鍵啊,基本上每行代碼我都添加了注釋?zhuān)?dāng)然了,為了確保大家都能搞清楚這個(gè)類(lèi)到底做了什么,我再來(lái)畫(huà)一幅流程圖,這樣就一清二楚了。
SpringSecurity 是一個(gè)安全管理框架,可以和 Spring Boot 應(yīng)用無(wú)縫銜接,SecurityContextHolder 是其中非常關(guān)鍵的一個(gè)工具類(lèi),持有安全上下文信息,里面保存有當(dāng)前操作的用戶(hù)是誰(shuí),用戶(hù)是否已經(jīng)被認(rèn)證,用戶(hù)擁有的權(quán)限等關(guān)鍵信息。
SecurityContextHolder 默認(rèn)使用了 ThreadLocal 策略來(lái)存儲(chǔ)認(rèn)證信息,ThreadLocal 的特點(diǎn)是存在它里邊的數(shù)據(jù),哪個(gè)線程存的,哪個(gè)線程才能訪問(wèn)到。這就意味著不同的請(qǐng)求進(jìn)入到服務(wù)器端后,會(huì)由不同的 Thread 去處理,例如線程 A 將請(qǐng)求 1 的用戶(hù)信息存入了 ThreadLocal,線程 B 在處理請(qǐng)求 2 的時(shí)候是無(wú)法獲取到用戶(hù)信息的。
所以說(shuō) JwtAuthenticationTokenFilter 過(guò)濾器會(huì)在每次請(qǐng)求過(guò)來(lái)的時(shí)候進(jìn)行一遍 JWT 的驗(yàn)證,確??蛻?hù)端過(guò)來(lái)的請(qǐng)求是安全的。然后 SpringSecurity 才會(huì)對(duì)接下來(lái)的請(qǐng)求接口放行。這也是 JWT 和 Session 的根本區(qū)別:
- JWT 需要每次請(qǐng)求的時(shí)候驗(yàn)證一次,并且只要 JWT 沒(méi)有過(guò)期,哪怕服務(wù)器端重啟了,認(rèn)證仍然有效。
- Session 在沒(méi)有過(guò)期的情況下是不需要重新對(duì)用戶(hù)信息進(jìn)行驗(yàn)證的,當(dāng)服務(wù)器端重啟后,用戶(hù)需要重新登錄獲取新的 Session。
也就是說(shuō),在 JWT 的方案下,服務(wù)器端保存的密鑰(secret)一定不能泄露,否則客戶(hù)端就可以根據(jù)簽名算法偽造用戶(hù)的認(rèn)證信息了。
三、Swagger 中添加 JWT 驗(yàn)證
第一步,訪問(wèn) login 接口,輸入用戶(hù)名和密碼進(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ì)通過(guò) JwtAuthenticationTokenFilter 過(guò)濾器對(duì) JWT 進(jìn)行校驗(yàn)。
到此為止,整個(gè)流程全部打通了,完美!
四、總結(jié)
綜上來(lái)看,用 JWT 來(lái)解決前后端分離項(xiàng)目中的跨域認(rèn)證還是非常絲滑的,這主要得益于 JSON 的通用性,可以跨語(yǔ)言,JavaScript 和 Java 都支持;另外,JWT 的組成非常簡(jiǎn)單,非常便于傳輸;還有 JWT 不需要在服務(wù)器端保存會(huì)話(huà)信息(Session),非常易于擴(kuò)展。
當(dāng)然了,為了保證 JWT 的安全性,不要在 JWT 中保存敏感信息,因?yàn)橐坏┧借€泄露,JWT 是很容易在客戶(hù)端被解密的;如果可以,請(qǐng)使用 HTTPS 協(xié)議。
網(wǎng)站題目:再見(jiàn)Session!這個(gè)跨域認(rèn)證解決方案真的優(yōu)雅!
網(wǎng)頁(yè)地址:http://fisionsoft.com.cn/article/cdcsdoo.html


咨詢(xún)
建站咨詢(xún)
