SpringSecurity整合jwt權(quán)限認(rèn)證的全流程講解
JWT
本文代碼截取自實(shí)際項(xiàng)目。
jwt(Json Web Token),一個(gè)token,令牌。
簡(jiǎn)單流程:
用戶登錄成功后,后端返回一個(gè)token,也就是頒發(fā)給用戶一個(gè)憑證。之后每一次訪問(wèn),前端都需要攜帶這個(gè)token,后端通過(guò)token來(lái)解析出當(dāng)前訪問(wèn)對(duì)象。
優(yōu)點(diǎn)
1、一定程度上解放了后端,后端不需要再記錄當(dāng)前用戶是誰(shuí),不需要再維護(hù)一個(gè)session,節(jié)省了開(kāi)銷。
2、session依賴于cookie,某些場(chǎng)合cookie是用不了的,比如用戶瀏覽器cookie被禁用、移動(dòng)端無(wú)法存儲(chǔ)cookie等。
缺點(diǎn)
1、既然服務(wù)器通過(guò)token就可以知道當(dāng)前用戶是誰(shuí),說(shuō)明其中包含了用戶信息,有一定的泄露風(fēng)險(xiǎn)。
2、因?yàn)槭菬o(wú)狀態(tài)的,服務(wù)器不維持會(huì)話,那么每一次請(qǐng)求都會(huì)重新去數(shù)據(jù)庫(kù)讀取權(quán)限信息,性能有一定影響。
(如果想提高性能,可以將權(quán)限數(shù)據(jù)存到redis,但是如果用redis,就已經(jīng)失去了jwt的優(yōu)點(diǎn),直接用普通的token+redis即可)
3、只能校驗(yàn)token是否正確,通過(guò)設(shè)定過(guò)期時(shí)間來(lái)確定其有效性,不可以手動(dòng)注銷。
先說(shuō)怎么做,再說(shuō)為什么。
代碼
依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<security.version>2.3.3.RELEASE</security.version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<jwt.version>0.9.1</jwt.version>
</dependency>
jwt工具類
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = -3369436201465044824L;
//生成token
public static String createToken(String username) {
return Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis()+ Constants.Jwt.EXPIRATION))
.signWith(SignatureAlgorithm.HS512, Constants.Jwt.KEY).compressWith(CompressionCodecs.GZIP).compact();
}
//獲取用戶名
public static String getUserName(String token) {
return Jwts.parser().setSigningKey(Constants.Jwt.KEY).parseClaimsJws(token).getBody().getSubject();
}
}
涉及常量
public interface Constants {
public interface Jwt{
/**
* 密鑰
*/
String KEY = "123123";
/**
* 過(guò)期時(shí)間
*/
long EXPIRATION = 7200000;
/**
* 請(qǐng)求頭
*/
String TOKEN_HEAD = "Authorization";
}
}
實(shí)體類和Service
為了減少對(duì)實(shí)體類的入侵,我又定義了一個(gè)對(duì)象
原實(shí)體類
/**
* 用戶信息
*
*/
@Data
@TableName("tb_user_info")
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主鍵id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 登陸賬號(hào)
*/
private String loginName;
/**
* 顯示名
*/
private String dispName;
/**
* 密碼
*/
private String password;
/**
* 狀態(tài),1正常,2禁用
*/
private Byte status;
/**
* 創(chuàng)建人
*/
@TableField(fill = FieldFill.INSERT)
private Integer createBy;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Integer updateBy;
/**
* 創(chuàng)建時(shí)間
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新時(shí)間
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 1正常, 0 刪除
*/
@TableLogic
private Byte deleted;
}
定義對(duì)象
@Data
public class UserSecurity implements UserDetails {
private static final long serialVersionUID = -6777760550924505136L;
private UserInfo userInfo;
private List<String> permissionValues;
public UserSecurity(UserInfo userInfo) {
this.userInfo = userInfo;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValues) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return userInfo.getPassword();
}
@Override
public String getUsername() {
return userInfo.getLoginName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
service
@Service
public class UserInfoServiceImpl implements UserInfoService, UserDetailsService {
@Autowired
UserInfoMapper userInfoMapper;
@Autowired
PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("login_name", username);
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
List<String> rights = permissionService.listPermissions(username);
UserSecurity userSecurity = new UserSecurity(userInfo);
userSecurity.setPermissionValues(rights);
return userSecurity;
}
}
配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserInfoServiceImpl userInfoService;
@Autowired
JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter;
@Autowired
TokenLoginFilter tokenLoginFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoderBean());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and().csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/hello").permitAll()
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/*").permitAll()
.antMatchers("/csrf").permitAll()
.antMatchers("/doc.html").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.addFilterAt(tokenLoginFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jwtAuthorizationTokenFilter, TokenLoginFilter.class).httpBasic()
;
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
過(guò)濾器
一個(gè)負(fù)責(zé)登錄
一個(gè)負(fù)責(zé)鑒權(quán)
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
@Autowired
PermissionService permissionService;
@Autowired
UserInfoServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//獲取當(dāng)前認(rèn)證成功用戶權(quán)限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//判斷如果有權(quán)限信息,放到權(quán)限上下文中
if (authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//從header獲取token
String token = request.getHeader(Constants.Jwt.TOKEN_HEAD);
if (token != null) {
//從token獲取用戶名
String loginName = JwtTokenUtils.getUserName(token);
//獲取權(quán)限列表
UserInfo userInfo = userDetailsService.selectByLoginName(loginName);
List<String> permissions = permissionService.listPermissions(loginName);
Collection<GrantedAuthority> authority = new ArrayList<>();
for (String permissionValue : permissions) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
return new UsernamePasswordAuthenticationToken(userInfo, token, authority);
}
return null;
}
}
Component
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
public TokenLoginFilter() {
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//獲取表單提交數(shù)據(jù)
try {
UserInfo user = new ObjectMapper().readValue(request.getInputStream(), UserInfo.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getLoginName(), user.getPassword(),null);
return super.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserSecurity userSecurity = (UserSecurity) authResult.getPrincipal();
String token = JwtTokenUtils.createToken(userSecurity.getUsername());
ResponseUtils.out(response, R.ok(token));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtils.out(response, R.fail(ServiceError.LOGIN_FAIL));
}
@Autowired
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
}
異常處理
登陸失敗異常處理,自定義返回類型
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtils.out(response, R.fail(ServiceError.AUTHENTICATION_FAIL));
}
}
至于權(quán)限和角色,得看具體的業(yè)務(wù),我這邊就不貼出來(lái)了。
流程分析
總體分析
代碼肯定不是憑空出來(lái)的,必定有章可循。
首先看網(wǎng)上找的這個(gè)圖:

最前面兩個(gè)過(guò)濾器:密碼檢驗(yàn)、權(quán)限認(rèn)證。(順序很重要,鑒權(quán)之前總得給人家輸入賬號(hào)密碼的機(jī)會(huì)吧)
想一下傳統(tǒng)的session會(huì)話模式,登錄成功后,服務(wù)器維護(hù)了session,瀏覽器得cookie里存放了sessionId,用戶訪問(wèn)的時(shí)候?yàn)g覽器自動(dòng)帶上sessionId。
放在以前,服務(wù)器端的工作都是是框架幫我們做了(原生session來(lái)自于tomcat)。
換成jwt后,需要前端自己去存儲(chǔ)token,后臺(tái)自己來(lái)解析token。
但是流程還是一樣的,用戶登錄、獲取token、攜帶token、校驗(yàn)token。。。。。。
登錄校驗(yàn)過(guò)濾器分析
分析完流程后,就需要看看,如果不用jwt,框架自身是如何實(shí)現(xiàn)的。
找到UsernamePasswordAuthenticationFilter這個(gè)類

可以發(fā)現(xiàn)這個(gè)類主要是用來(lái)檢驗(yàn)密碼生成,憑證的。那我們只要繼承這個(gè)UsernamePasswordAuthenticationFilter類,重寫(xiě)attemptAuthentication,就可以實(shí)現(xiàn)登錄校驗(yàn)功能。
所以我們的代碼是這樣的:

權(quán)限認(rèn)證過(guò)濾器分析
需要看一下BasicAuthenticationFilter的源碼
![]()
其中的關(guān)鍵方法:

思考一下,我們?nèi)绻枰脩粜畔?,?huì)從哪里???
自然是請(qǐng)求頭,前端訪問(wèn)的時(shí)候,會(huì)把token放在請(qǐng)求頭。
取得token后,就要開(kāi)始解析token了。token正確,將認(rèn)證信息以及權(quán)限信息注入,token不正確則可以拋出異常。這就是我們第二個(gè)過(guò)濾器的由來(lái)

一個(gè)地方需要注意一下:
這邊的userInfo,就是對(duì)應(yīng)的principal
SecurityContextHolder.getContext().getAuthentication().getPrincipal();

比如這么用:
@Component
public class SecurityUtils {
/**
* 獲取當(dāng)前用戶信息
* @return 用戶信息
*/
public UserInfo getCurrentUser() {
return (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
這里的UserInfo對(duì)象,就是principal。
如果你上面放的不對(duì)象,而是用戶名或者id,那么獲取principal的時(shí)候,自然是對(duì)應(yīng)的用戶名或者id了。
種瓜得瓜,種豆得豆。
配置文件
截取核心片段加點(diǎn)注釋。

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- SpringSecurity6.0 如何通過(guò)JWTtoken進(jìn)行認(rèn)證授權(quán)
- springSecurity自定義登錄接口和JWT認(rèn)證過(guò)濾器的流程
- SpringSecurity+jwt+captcha登錄認(rèn)證授權(quán)流程總結(jié)
- SpringSecurity+Redis+Jwt實(shí)現(xiàn)用戶認(rèn)證授權(quán)
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫(kù)登錄認(rèn)證的實(shí)現(xiàn)
- SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringBoot整合SpringSecurity實(shí)現(xiàn)JWT認(rèn)證的項(xiàng)目實(shí)踐
- SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實(shí)現(xiàn)
- SpringSecurity JWT基于令牌的無(wú)狀態(tài)認(rèn)證實(shí)現(xiàn)
相關(guān)文章
使用SpringBoot配置多數(shù)據(jù)源的經(jīng)驗(yàn)分享
這篇文章主要介紹了使用SpringBoot配置多數(shù)據(jù)源的經(jīng)驗(yàn)分享,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04
SpringBoot如何動(dòng)態(tài)改變?nèi)罩炯?jí)別
這篇文章主要介紹了SpringBoot如何動(dòng)態(tài)改變?nèi)罩炯?jí)別,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2020-12-12
MyBatis中#{}占位符與${}拼接符的用法說(shuō)明
這篇文章主要介紹了MyBatis中#{}占位符與${}拼接符的用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02
springboot如何接收復(fù)雜參數(shù)(同時(shí)接收J(rèn)SON與文件)
文章介紹了在Spring Boot中同時(shí)處理JSON和文件上傳時(shí)使用`@RequestPart`注解的方法,`@RequestPart`可以接收多種格式的參數(shù),包括JSON和文件,并且可以作為`multipart/form-data`格式中的key2025-02-02
Struts2開(kāi)發(fā) 基本配置與類型轉(zhuǎn)換
本篇文章,小編將為大家介紹關(guān)于Struts2開(kāi)發(fā) 基本配置與類型轉(zhuǎn)換,有需要的朋友可以參考一下2013-04-04
MyBatis-Plus 修改和添加自動(dòng)填充時(shí)間方式
這篇文章主要介紹了MyBatis-Plus 修改和添加自動(dòng)填充時(shí)間方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

