SpringSecurity-14-SpringSecurity結(jié)合JWT實現(xiàn)前后端分離的后端授權(quán)
什么是JWT
JWT是JSON WEB TOKEN的縮寫,它是基于RFC 7519標(biāo)準(zhǔn)定義的一種可以安全傳輸?shù)腏SON對象,因為使用了數(shù)字簽名,所以可以信任。
JWT的組成
JWT token的格式:header.payload.signature
-
header中用于存放簽名的生成算法
{"alg":?"HS512"} -
payload用于存放用戶名、token的生成時間和過期時間
{"sub":"admin","created":1489079981393,"exp":1489684781} -
signature為以header和payload生成的簽名,一旦header和payload被篡改,驗證將失敗
//secret為加密算法的密鑰
String?signature?=?HMACSHA512(base64UrlEncode(header)?+?"."?+base64UrlEncode(payload),secret)
JWT實例
這是一個JWT的字符串
eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NDg5ODg1MjAsInN1YiI6ImFkbWluIiwiY3JlYXRlZCI6MTY0ODk4NDkyMDQyNX0.P8YJ5AhcKATEpUmdtSmzGXcdDacESZ2jqU20JpjCqZOqy5AEE2uelYtay--Kg2wRWFx3bBhf9A5Jbv2S8fbs_A可以在該網(wǎng)站上獲得解析結(jié)果:https://jwt.io/

編碼實現(xiàn)
環(huán)境準(zhǔn)備工作
- 建立Spring Boot項目并集成了Spring Security,項目可以正常啟動
- 通過controller寫一個HTTP的GET方法服務(wù)接口,比如:“/student/selectall”
- 實現(xiàn)最基本的動態(tài)數(shù)據(jù)驗證及權(quán)限分配,即實現(xiàn)UserDetailsService接口。這兩個接口都是向Spring Security提供用戶、角色、權(quán)限等校驗信息的接口
- 如果你學(xué)習(xí)過Spring Security的formLogin登錄模式,請將HttpSecurity配置中的formLogin()配置段全部去掉。因為JWT完全使用JSON接口,沒有from表單提交。
- HttpSecurity配置中一定要加上csrf().disable(),即暫時關(guān)掉跨站攻擊CSRF的防御。這樣是不安全的,我們后續(xù)章節(jié)再做處理。
以上實現(xiàn)可以去查看我的專題SpringBoot和SpringSecurity進(jìn)行查看。

在pom.xml中添加項目依賴
?<dependencies>
??????<!--springsecurity-->
????????<dependency>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-starter-security</artifactId>
????????</dependency>
????????<!--jwt-->
????????<dependency>
????????????<groupId>io.jsonwebtoken</groupId>
????????????<artifactId>jjwt</artifactId>
????????????<version>0.9.0</version>
????????</dependency>
????????<dependency>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-starter-web</artifactId>
????????</dependency>
????????<!--mysql驅(qū)動-->
????????<dependency>
????????????<groupId>mysql</groupId>
????????????<artifactId>mysql-connector-java</artifactId>
????????????<scope>runtime</scope>
????????</dependency>
????????<!--mybatis-plus-->
????????<dependency>
????????????<groupId>com.baomidou</groupId>
????????????<artifactId>mybatis-plus-boot-starter</artifactId>
????????????<version>3.5.1</version>
????????</dependency>
????????<dependency>
????????????<groupId>cn.hutool</groupId>
????????????<artifactId>hutool-all</artifactId>
????????????<version>5.5.7</version>
????????</dependency>
????????<dependency>
????????????<groupId>org.projectlombok</groupId>
????????????<artifactId>lombok</artifactId>
????????????<optional>true</optional>
????????</dependency>
????????<dependency>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-starter-test</artifactId>
????????????<scope>test</scope>
????????</dependency>
????????<dependency>
????????????<groupId>org.springframework.security</groupId>
????????????<artifactId>spring-security-test</artifactId>
????????????<scope>test</scope>
????????</dependency>
????</dependencies>在application.yml中加入如下自定義一些關(guān)于JWT的配置
jwt:
??header:?JWTName?
??secret:?springkhbd
??expiration:?360-
jwt.header的value是Http的header中存儲JWT的名稱,名字可讀性越差越安全 -
jwt.secret用來對JWT基礎(chǔ)信息進(jìn)行加密和解密的密匙。 -
jwt.expiration用來設(shè)置JWT令牌的有效時間
添加JWT token的工具類JwtTokenUtil
JwtTokenUtil用于生成和解析JWT token的工具類
主要方法:
- generateToken(UserDetails userDetails):根據(jù)用戶信息生成token令牌
- getUserNameFromToken(String token):根據(jù)token令牌獲取用戶名
- validateToken(String token, UserDetails userDetails):判斷用戶是否過期
- refreshToken(String token):根據(jù)token屬性token的過期時間
package?com.security.learn.util;
import?io.jsonwebtoken.Claims;
import?io.jsonwebtoken.Jwts;
import?io.jsonwebtoken.SignatureAlgorithm;
import?lombok.Data;
import?lombok.extern.slf4j.Slf4j;
import?org.springframework.boot.context.properties.ConfigurationProperties;
import?org.springframework.security.core.userdetails.UserDetails;
import?org.springframework.stereotype.Component;
import?java.util.Date;
import?java.util.HashMap;
import?java.util.Map;
@Data
@Slf4j
@ConfigurationProperties(prefix?=?"jwt")
@Component
public?class?JwtTokenUtil?{
????private?static?final?String?CLAIM_KEY_USERNAME?=?"sub";
????private?static?final?String?CLAIM_KEY_CREATED?=?"created";
????private?String?secret;
????private?Long?expiration;
????private?String?header;
????/**
?????*?生成token令牌
?????*
?????*?@param?userDetails?用戶
?????*?@return?令token牌?????*/
????public?String?generateToken(UserDetails?userDetails)?{
????????Map<String,?Object>?claims?=?new?HashMap<>(2);
????????claims.put(CLAIM_KEY_CREATED,?userDetails.getUsername());
????????claims.put(CLAIM_KEY_CREATED,?new?Date());
????????//生成Token
????????return?generateToken(claims);
????}
????/**
?????*?從claims生成令牌
?????*?@param?claims
?????*?@return?????*/
????private?String?generateToken(Map<String,?Object>?claims)?{
????????return?Jwts.builder()
????????????????.setClaims(claims)
????????????????.setExpiration(generateExpirationDate())
????????????????.signWith(SignatureAlgorithm.HS512,?secret)
????????????????.compact();
????}
????/**
?????*?從Token中獲取用戶名稱
?????*?@param?token
?????*?@return?????*/
????public?String?getUserNameFromToken(String?token)?{
????????String?username;
????????try?{
????????????Claims?claims?=?getClaimsFromToken(token);
????????????username?=??claims.getSubject();
????????}?catch?(Exception?e)?{
????????????username?=?null;
????????}
????????return?username;
????}
????/**
?????*?從令牌中獲取數(shù)據(jù)聲明,如果看不懂就看誰調(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)?{
????????????log.info("JWT格式驗證失?。簕}",token);
????????????claims?=?null;
????????}
????????return?claims;
????}
????/**
?????*?生成token的過期時間?????*/
????private?Date?generateExpirationDate()?{
????????return?new?Date(System.currentTimeMillis()?+?expiration?*?1000);
????}
????/**
?????*?根據(jù)token過去過期時間
?????*?@param?token
?????*?@return?????*/
????private?Date?getExpiredDateFromToken(String?token)?{
????????Claims?claims?=?getClaimsFromToken(token);
????????return?claims.getExpiration();
????}
????/**
?????*
?????*?驗證Token是否過期
?????*?@param?token
?????*?@param?userDetails
?????*?@return?true表示沒有過期,false表示過期?????*/
????public?boolean?validateToken(String?token,?UserDetails?userDetails)?{
????????String?username?=?getUserNameFromToken(token);
????????return?username.equals(userDetails.getUsername())?&&?!isTokenExpired(token);
????}
????/**
?????*?判斷令牌是否過期
?????*?@param?token
?????*?@return?????*/
????public?Boolean?isTokenExpired(String?token)?{
????????try?{
????????????Claims?claims?=?getClaimsFromToken(token);
????????????Date?expiration?=?claims.getExpiration();
????????????return?expiration.before(new?Date());
????????}?catch?(Exception?e)?{
????????????return?false;
????????}
????}
????/**
?????*?判斷token是否可以刷新
?????*?@param?token
?????*?@return?????*/
????public?boolean?canRefresh(String?token)?{
????????return?!isTokenExpired(token);
????}
????/**
?????*?刷新token?????*/
????public?String?refreshToken(String?token)?{
????????Claims?claims?=?getClaimsFromToken(token);
????????claims.put(CLAIM_KEY_CREATED,?new?Date());
????????return?generateToken(claims);
????}
}UserDetailsService接口的實現(xiàn)
@Component("myUserDetailsService")
@Slf4j
public?class?MyUserDetailsService?implements?UserDetailsService?{
????@Autowired
????private?UserMapper?userMapper;
????@Autowired
????private?AuthoritiesMapper?authoritiesMapper;
????@Override
????public?UserDetails?loadUserByUsername(String?username)?throws?UsernameNotFoundException?{
????????log.info("認(rèn)證請求:?"+?username);
????????QueryWrapper<UserEntity>?wrapper?=?new?QueryWrapper<>();
????????wrapper.eq("username",username);
????????List<UserEntity>?userEntities?=?userMapper.selectList(wrapper);
????????if?(userEntities.size()>0){
????????????QueryWrapper<AuthoritiesEntity>?wrapper1?=?new?QueryWrapper<>();
????????????wrapper.eq("userId",?userEntities.get(0).getId());
????????????List<AuthoritiesEntity>?authorities?=?authoritiesMapper.selectList(wrapper1);
????????????return?new?User(username,?userEntities.get(0).getPassword(),?AuthorityUtils.createAuthorityList(authorities.toString()));
????????}
????????return?null;
????}
}開發(fā)登錄接口(獲取Token的接口)
JwtAdminService接口
public?interface?JwtAdminService?{
????/**
?????*?登錄功能
?????*?@param?username?用戶名
?????*?@param?password?密碼
?????*?@return?生成的JWT的token?????*/
????String?login(String?username,?String?password);
????/**
?????*?刷新Token
?????*?@param?oldToken
?????*?@return?????*/
????String?refreshToken(String?oldToken);
}
JwtAdminService接口實現(xiàn)
@AllArgsConstructor
@Slf4j
@Service
public?class?JwtAdminServiceImpl?implements?JwtAdminService?{
????private?final?UserDetailsService?customUserDetailsService;
????private?final?JwtTokenUtil?jwtTokenUtill;
????private?final?PasswordEncoder?passwordEncoder;
????/**
?????*?根據(jù)用戶名密碼登錄時生成Token
?????*?@param?username?用戶名
?????*?@param?password?密碼
?????*?@return?????*/
????@Override
????public?String?login(String?username,?String?password)?{
????????try{
????????????//根據(jù)用戶名獲取?用戶信息
????????????UserDetails?userDetails?=?customUserDetailsService.loadUserByUsername(username);
????????????if(!passwordEncoder.matches(password,userDetails.getPassword())){
????????????????throw?new?BadCredentialsException("密碼不正確");
????????????}
????????????UsernamePasswordAuthenticationToken?token?=?new?UsernamePasswordAuthenticationToken(username,?password);
????????????SecurityContextHolder.getContext().setAuthentication(token);
????????}catch?(AuthenticationException?e){
????????????log.error("用戶名或者密碼不正確");
????????}
????????//生成JWT
????????UserDetails?userDetails?=?customUserDetailsService.loadUserByUsername(?username?);
????????return?jwtTokenUtill.generateToken(userDetails);
????}
????@Override
????public?String?refreshToken(String?oldToken)?{
????????if?(!jwtTokenUtill.isTokenExpired(oldToken))?{
????????????return?jwtTokenUtill.refreshToken(oldToken);
????????}
????????return?null;
????}
}JwtAuthController的實現(xiàn)
- "/login"接口用于登錄驗證,并且生成JWT返回給客戶端
- "/refreshtoken"接口用于刷新JWT,更新JWT令牌的有效期
@RestController
public?class?JwtAuthController?{
????@Resource
????private?JwtAdminService?jwtAuthService;
????@PostMapping(value?=?"/login")
????public?Result?login(@RequestBody?Map<String,?String>?map)?throws?Exception?{
????????String?username?=?map.get("username");
????????String?password?=?map.get("password");
????????if?(StrUtil.isEmpty(username)?||?(StrUtil.isEmpty(password)))?{
????????????return?Result.fail("用戶名密碼不能為空");
????????}
????????try{
????????????return?Result.data(?jwtAuthService.login(username,?password));
????????}catch(Exception?e){
????????????return?Result.fail(e.getMessage());
????????}
????}
????@PostMapping(value?=?"/refreshtoken")
????public?String?refresh(@RequestHeader("${jwt.header}")?String?token)?{
????????return?jwtAuthService.refreshToken(token);
????}
}
添加SpringSecurity的配置類LearnSrpingSecurity
import?com.security.learn.filter.JwtAuthenticationTokenFilter;
import?com.security.learn.handler.RestAuthenticationEntryPoint;
import?com.security.learn.handler.RestfulAccessDeniedHandler;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import?org.springframework.security.config.annotation.web.builders.HttpSecurity;
import?org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import?org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import?org.springframework.security.config.http.SessionCreationPolicy;
import?org.springframework.security.core.userdetails.UserDetailsService;
import?org.springframework.security.crypto.password.PasswordEncoder;
import?org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
?*?安全配置類?*/
@EnableWebSecurity
public?class?LearnSrpingSecurity?extends?WebSecurityConfigurerAdapter?{
????@Autowired
????private?UserDetailsService?myUserDetailsService;
????@Autowired
????private?JwtAuthenticationTokenFilter?jwtAuthenticationTokenFilter;
????@Autowired
????private?RestfulAccessDeniedHandler?restfulAccessDeniedHandler;
????@Autowired
????private?RestAuthenticationEntryPoint?restAuthenticationEntryPoint;
????/**
?????*?認(rèn)證管理器
?????*?1.認(rèn)證信息提供方式(用戶名、密碼、當(dāng)前用戶的資源權(quán)限)
?????*?2.可采用內(nèi)存存儲方式,也可能采用數(shù)據(jù)庫方式等
?????*?@param?auth
?????*?@throws?Exception?????*/
????@Override
????protected?void?configure(AuthenticationManagerBuilder?auth)?throws?Exception?{
????????auth.userDetailsService(myUserDetailsService);
????}
????/**
?????*?資源權(quán)限配置(過濾器鏈):
?????*?1、被攔截的資源
?????*?2、資源所對應(yīng)的角色權(quán)限
?????* 3、定義認(rèn)證方式:httpBasic 、httpForm
?????*?4、定制登錄頁面、登錄請求地址、錯誤處理方式
?????*?5、自定義?spring?security?過濾器
?????*?@param?http
?????*?@throws?Exception?????*/
????@Override
????protected?void?configure(HttpSecurity?http)?throws?Exception?{
????????http.csrf().disable()?//禁用跨站csrf攻擊防御,后面的章節(jié)會專門講解
????????????????.sessionManagement()//?基于token,所以不需要session
????????????????.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
????????????????.and()
????????????????.addFilterBefore(jwtAuthenticationTokenFilter,?UsernamePasswordAuthenticationFilter.class)
????????????????.authorizeRequests()
????????????????.antMatchers("/login").permitAll()//不需要通過登錄驗證就可以被訪問的資源路徑
????????????????.anyRequest().authenticated();
????????//添加自定義未授權(quán)和未登錄結(jié)果返回
????????http.exceptionHandling()
????????????????.accessDeniedHandler(restfulAccessDeniedHandler)
????????????????.authenticationEntryPoint(restAuthenticationEntryPoint);
????}
}
相關(guān)依賴以及方法說明
-
configure(HttpSecurity http):資源權(quán)限配置(過濾器鏈)、jwt過濾器及出異常后的處理器; -
configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder; -
RestfulAccessDeniedHandler:當(dāng)用戶沒有訪問權(quán)限時的處理器,用于返回JSON格式的處理結(jié)果; -
RestAuthenticationEntryPoint:當(dāng)未登錄或token失效時,返回JSON格式的結(jié)果; -
UserDetailsService:SpringSecurity定義的核心接口,用于根據(jù)用戶名獲取用戶信息,需要自行實現(xiàn); -
JwtAuthenticationTokenFilter:在用戶名和密碼校驗前添加的過濾器,如果有jwt的token,會自行根據(jù)token信息進(jìn)行登錄。 - configure(HttpSecurity http),主要配置:

- 將我們的自定義jwtAuthenticationTokenFilter,加載到UsernamePasswordAuthenticationFilter的前面。
- 因為我們使用了JWT,表明了我們的應(yīng)用是一個前后端分離的應(yīng)用,所以我們可以開啟STATELESS禁止使用session
添加RestfulAccessDeniedHandler
當(dāng)訪問接口沒有權(quán)限時,自定義的返回結(jié)果
/**
?*?當(dāng)訪問接口沒有權(quán)限時,自定義的返回結(jié)果?*/
@Component
public?class?RestfulAccessDeniedHandler?implements?AccessDeniedHandler?{
????@Override
????public?void?handle(HttpServletRequest?request,
???????????????????????HttpServletResponse?response,???????????????????????AccessDeniedException?e)?throws?IOException,?ServletException?{
????????response.setCharacterEncoding("UTF-8");
????????response.setContentType("application/json");
????????response.getWriter().println(JSONUtil.parse(Result.fail(e.getMessage())));
????????response.getWriter().flush();
????}
}添加RestAuthenticationEntryPoint
當(dāng)用戶未登錄或者token失效訪問接口時,自定義的返回結(jié)果
/**
?*?當(dāng)未登錄或者token失效訪問接口時,自定義的返回結(jié)果
?*/
@Component
public?class?RestAuthenticationEntryPoint?implements?AuthenticationEntryPoint?{
????@Override
????public?void?commence(HttpServletRequest?request,?HttpServletResponse?response,?AuthenticationException?authException)?throws?IOException,?ServletException?{
????????response.setCharacterEncoding("UTF-8");
????????response.setContentType("application/json");
????????response.getWriter().println(JSONUtil.parse(Result.fail(authException.getMessage())));
????????response.getWriter().flush();
????}
}添加JwtAuthenticationTokenFilter
在用戶名和密碼校驗前添加的過濾器,如果請求中有jwt的token且有效,會取出token中的用戶名,然后調(diào)用SpringSecurity的API進(jìn)行登錄操作。
@Slf4j
@Component
@AllArgsConstructor
public?class?JwtAuthenticationTokenFilter?extends?OncePerRequestFilter?{
????private?final?UserDetailsService?myUserDetailsService;
????private?final?JwtTokenUtil?jwtTokenUtil;
????@Override
????protected?void?doFilterInternal(HttpServletRequest?request,?HttpServletResponse?response,?FilterChain?filterChain)?throws?ServletException,?IOException?{
????????String?jwt?=?request.getHeader(jwtTokenUtil.getHeader());
????????if(!StrUtil.isEmpty(jwt)){
????????????//根據(jù)jwt獲取用戶名
????????????String?username?=?jwtTokenUtil.getUserNameFromToken(jwt);
????????????log.info("校驗username:{}",username);
????????????//如果可以正確從JWT中提取用戶信息,并且該用戶未被授權(quán)
????????????if(!StrUtil.isEmpty(username)?&&?SecurityContextHolder.getContext().getAuthentication()==null){
????????????????UserDetails?userDetails?=?this.myUserDetailsService.loadUserByUsername(username);
????????????????if(jwtTokenUtil.validateToken(jwt,userDetails)){
????????????????????//給使用該JWT令牌的用戶進(jìn)行授權(quán)
????????????????????UsernamePasswordAuthenticationToken?authenticationToken
????????????????????????????=?new?UsernamePasswordAuthenticationToken(userDetails,null,
????????????????????????????userDetails.getAuthorities());
????????????????????authenticationToken.setDetails(new?WebAuthenticationDetailsSource().buildDetails(request));
????????????????????SecurityContextHolder.getContext().setAuthentication(authenticationToken);
????????????????}
????????????}
????????}
????????filterChain.doFilter(request,?response);
????}
}測試
測試登錄接口,即:獲取token的接口。輸入正確的用戶名、密碼即可獲取token

- 使用不帶token,但是不傳遞JWT令牌,結(jié)果是禁止訪問

- 使用不帶token,攜帶JWT令牌

如果您覺得本文不錯,歡迎關(guān)注,點贊,收藏支持,您的關(guān)注是我堅持的動力!
原創(chuàng)不易,轉(zhuǎn)載請注明出處,感謝支持!如果本文對您有用,歡迎轉(zhuǎn)發(fā)分享!
關(guān)注公眾號 springboot葵花寶典 我將持續(xù)更新,并且獲取我搜集的spingboot資料,謝謝!