Spring Security代碼實(shí)現(xiàn)JWT接口權(quán)限授予與校驗(yàn)功能
通過(guò)筆者前兩篇文章的說(shuō)明,相信大家已經(jīng)知道JWT是什么,怎么用,該如何結(jié)合Spring Security使用。那么本節(jié)就用代碼來(lái)具體的實(shí)現(xiàn)一下JWT登錄認(rèn)證及鑒權(quán)的流程。
一、環(huán)境準(zhǔn)備工作
- 建立Spring Boot項(xiàng)目并集成了Spring Security,項(xiàng)目可以正常啟動(dòng)
- 通過(guò)controller寫(xiě)一個(gè)HTTP的GET方法服務(wù)接口,比如:“/hello”
- 實(shí)現(xiàn)最基本的動(dòng)態(tài)數(shù)據(jù)驗(yàn)證及權(quán)限分配,即實(shí)現(xiàn)UserDetailsService接口和UserDetails接口。這兩個(gè)接口都是向Spring Security提供用戶(hù)、角色、權(quán)限等校驗(yàn)信息的接口
- 如果你學(xué)習(xí)過(guò)Spring Security的formLogin登錄模式,請(qǐng)將HttpSecurity配置中的formLogin()配置段全部去掉。因?yàn)镴WT完全使用JSON接口,沒(méi)有from表單提交。
- HttpSecurity配置中一定要加上
csrf().disable(),即暫時(shí)關(guān)掉跨站攻擊CSRF的防御。這樣是不安全的,我們后續(xù)章節(jié)再做處理。
以上的內(nèi)容,我們?cè)谥暗奈恼轮卸家呀?jīng)講過(guò)。如果仍然不熟悉,可以翻看本號(hào)之前的文章。
## 二、開(kāi)發(fā)JWT工具類(lèi)
通過(guò)maven坐標(biāo)引入JWT工具包jjwt
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
在application.yml中加入如下自定義一些關(guān)于JWT的配置
jwt: header: JWTHeaderName secret: aabbccdd expiration: 3600000
- 其中header是攜帶JWT令牌的HTTP的Header的名稱(chēng)。雖然我這里叫做JWTHeaderName,但是在實(shí)際生產(chǎn)中可讀性越差越安全。
- secret是用來(lái)為JWT基礎(chǔ)信息加密和解密的密鑰。雖然我在這里在配置文件寫(xiě)死了,但是在實(shí)際生產(chǎn)中通常不直接寫(xiě)在配置文件里面。而是通過(guò)應(yīng)用的啟動(dòng)參數(shù)傳遞,并且需要定期修改。
- expiration是JWT令牌的有效時(shí)間。
寫(xiě)一個(gè)Spring Boot配置自動(dòng)加載的工具類(lèi)。
@Data
@ConfigurationProperties(prefix = "jwt") //配置自動(dòng)加載,prefix是配置的前綴
@Component
public class JwtTokenUtil implements Serializable {
private String secret;
private Long expiration;
private String header;
/**
* 生成token令牌
*
* @param userDetails 用戶(hù)
* @return 令token牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 從令牌中獲取用戶(hù)名
*
* @param token 令牌
* @return 用戶(hù)名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判斷令牌是否過(guò)期
*
* @param token 令牌
* @return 是否過(guò)期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 驗(yàn)證令牌
*
* @param token 令牌
* @param userDetails 用戶(hù)
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
SysUser user = (SysUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
/**
* 從claims生成令牌,如果看不懂就看誰(shuí)調(diào)用它
*
* @param claims 數(shù)據(jù)聲明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 從令牌中獲取數(shù)據(jù)聲明,如果看不懂就看誰(shuí)調(diào)用它
*
* @param token 令牌
* @return 數(shù)據(jù)聲明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
上面的代碼就是使用io.jsonwebtoken.jjwt提供的方法開(kāi)發(fā)JWT令牌生成、刷新的工具類(lèi)。
三、開(kāi)發(fā)登錄接口(獲取Token的接口)
- "/authentication"接口用于登錄驗(yàn)證,并且生成JWT返回給客戶(hù)端
- "/REFRESHTOKEN"接口用于刷新JWT,更新JWT令牌的有效期
@RESTCONTROLLER
PUBLIC CLASS JWTAUTHCONTROLLER {
@RESOURCE
PRIVATE JWTAUTHSERVICE JWTAUTHSERVICE;
@POSTMAPPING(VALUE = "/AUTHENTICATION")
PUBLIC AJAXRESPONSE LOGIN(@REQUESTBODY MAP<STRING, STRING> MAP) {
STRING USERNAME = MAP.GET("USERNAME");
STRING PASSWORD = MAP.GET("PASSWORD");
IF (STRINGUTILS.ISEMPTY(USERNAME) || STRINGUTILS.ISEMPTY(PASSWORD)) {
RETURN AJAXRESPONSE.ERROR(
NEW CUSTOMEXCEPTION(CUSTOMEXCEPTIONTYPE.USER_INPUT_ERROR,"用戶(hù)名密碼不能為空"));
}
RETURN AJAXRESPONSE.SUCCESS(JWTAUTHSERVICE.LOGIN(USERNAME, PASSWORD));
}
@POSTMAPPING(VALUE = "/REFRESHTOKEN")
PUBLIC AJAXRESPONSE REFRESH(@REQUESTHEADER("${JWT.HEADER}") STRING TOKEN) {
RETURN AJAXRESPONSE.SUCCESS(JWTAUTHSERVICE.REFRESHTOKEN(TOKEN));
}
}
核心的token業(yè)務(wù)邏輯寫(xiě)在JwtAuthService 中
- login方法中首先使用用戶(hù)名、密碼進(jìn)行登錄驗(yàn)證。如果驗(yàn)證失敗拋出BadCredentialsException異常。如果驗(yàn)證成功,程序繼續(xù)向下走,生成JWT響應(yīng)給前端
- refreshToken方法只有在JWT token沒(méi)有過(guò)期的情況下才能刷新,過(guò)期了就不能刷新了。需要重新登錄。
@Service
public class JwtAuthService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
public String login(String username, String password) {
//使用用戶(hù)名密碼進(jìn)行登錄驗(yàn)證
UsernamePasswordAuthenticationToken upToken =
new UsernamePasswordAuthenticationToken( username, password );
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成JWT
UserDetails userDetails = userDetailsService.loadUserByUsername( username );
return jwtTokenUtil.generateToken(userDetails);
}
public String refreshToken(String oldToken) {
if (!jwtTokenUtil.isTokenExpired(oldToken)) {
return jwtTokenUtil.refreshToken(oldToken);
}
return null;
}
}
因?yàn)槭褂玫搅薃uthenticationManager ,所以在繼承WebSecurityConfigurerAdapter的SpringSecurity配置實(shí)現(xiàn)類(lèi)中,將AuthenticationManager 聲明為一個(gè)Bean。并將"/authentication"和 "/refreshtoken"開(kāi)放訪問(wèn)權(quán)限,如何開(kāi)放訪問(wèn)權(quán)限,我們之前的文章已經(jīng)講過(guò)了。
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
四、接口訪問(wèn)鑒權(quán)過(guò)濾器
當(dāng)用戶(hù)第一次登陸之后,我們將JWT令牌返回給了客戶(hù)端,客戶(hù)端應(yīng)該將該令牌保存起來(lái)。在進(jìn)行接口請(qǐng)求的時(shí)候,將令牌帶上,放到HTTP的header里面,header的名字要和jwt.header的配置一致,這樣服務(wù)端才能解析到。下面我們定義一個(gè)攔截器:
- 攔截接口請(qǐng)求,從請(qǐng)求request獲取token,從token中解析得到用戶(hù)名
- 然后通過(guò)UserDetailsService獲得系統(tǒng)用戶(hù)(從數(shù)據(jù)庫(kù)、或其他其存儲(chǔ)介質(zhì))
- 根據(jù)用戶(hù)信息和JWT令牌,驗(yàn)證系統(tǒng)用戶(hù)與用戶(hù)輸入的一致性,并判斷JWT是否過(guò)期。如果沒(méi)有過(guò)期,至此表明了該用戶(hù)的確是該系統(tǒng)的用戶(hù)。
- 但是,你是系統(tǒng)用戶(hù)不代表你可以訪問(wèn)所有的接口。所以需要構(gòu)造UsernamePasswordAuthenticationToken傳遞用戶(hù)、權(quán)限信息,并將這些信息通過(guò)authentication告知Spring Security。Spring Security會(huì)以此判斷你的接口訪問(wèn)權(quán)限。
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private MyUserDetailsService userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 從這里開(kāi)始獲取 request 中的 jwt token
String authHeader = request.getHeader(jwtTokenUtil.getHeader());
log.info("authHeader:{}", authHeader);
// 驗(yàn)證token是否存在
if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
// 根據(jù)token 獲取用戶(hù)名
String username = jwtTokenUtil.getUsernameFromToken(authHeader);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 通過(guò)用戶(hù)名 獲取用戶(hù)的信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 驗(yàn)證JWT是否過(guò)期
if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
//加載用戶(hù)、角色、權(quán)限信息,Spring Security根據(jù)這些信息判斷接口的訪問(wèn)權(quán)限
UsernamePasswordAuthenticationToken authentication
= new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
在spring Security的配置類(lèi)(即WebSecurityConfigurerAdapter實(shí)現(xiàn)類(lèi)的configure(HttpSecurity http)配置方法中,加入如下配置:
.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
- 因?yàn)槲覀兪褂昧薐WT,表明了我們的應(yīng)用是一個(gè)前后端分離的應(yīng)用,所以我們可以開(kāi)啟STATELESS禁止使用session。
- 當(dāng)然這并不絕對(duì),前后端分離的應(yīng)用通過(guò)一些辦法也是可以使用session的,這不是本文的核心內(nèi)容不做贅述。
- 將我們的自定義jwtAuthenticationTokenFilter,加載到UsernamePasswordAuthenticationFilter的前面。
五、測(cè)試一下:
測(cè)試登錄接口,即:獲取token的接口。輸入正確的用戶(hù)名、密碼即可獲取token。

下面我們?cè)L問(wèn)一個(gè)我們定義的簡(jiǎn)單的接口“/hello”,但是不傳遞JWT令牌,結(jié)果是禁止訪問(wèn)。當(dāng)我們將上一步返回的token,傳遞到header中,就能正常響應(yīng)hello的接口結(jié)果。

總結(jié)
以上所述是小編給大家介紹的Spring Security代碼實(shí)現(xiàn)JWT接口權(quán)限授予與校驗(yàn)功能,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
相關(guān)文章
@insert mybatis踩坑記錄,實(shí)體接收前端傳遞的參數(shù)
這篇文章主要介紹了@insert mybatis踩坑記錄,實(shí)體接收前端傳遞的參數(shù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
Java中forEach使用lambda表達(dá)式,數(shù)組和集合的區(qū)別說(shuō)明
這篇文章主要介紹了Java中forEach使用lambda表達(dá)式,數(shù)組和集合的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot整合JavaCV實(shí)現(xiàn)視頻截取第N幀并保存圖片
這篇文章主要為大家詳細(xì)介紹了springboot如何整合JavaCV實(shí)現(xiàn)視頻截取第N幀并保存為圖片,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-08-08
Springboot工具類(lèi)ReflectionUtils使用教程
這篇文章主要介紹了Springboot內(nèi)置的工具類(lèi)之ReflectionUtils的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-12-12
分布式醫(yī)療掛號(hào)系統(tǒng)SpringCache與Redis為數(shù)據(jù)字典添加緩存
這篇文章主要為大家介紹了分布式醫(yī)療掛號(hào)系統(tǒng)SpringCache與Redis為數(shù)據(jù)字典添加緩存,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
PowerJob LockService方法工作流程源碼解讀
這篇文章主要為大家介紹了PowerJob LockService方法工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
java.sql.Date和java.util.Date的區(qū)別詳解
Java中有兩個(gè)Date類(lèi),一個(gè)是java.util.Date通常情況下用它獲取當(dāng)前時(shí)間或構(gòu)造時(shí)間,另一個(gè)是java.sql.Date是針對(duì)SQL語(yǔ)句使用的,它只包含日期而沒(méi)有時(shí)間部分,這篇文章主要給大家介紹了關(guān)于java.sql.Date和java.util.Date區(qū)別的相關(guān)資料,需要的朋友可以參考下2023-03-03

