SpringSecurity整合jwt權(quán)限認(rèn)證的全流程講解
JWT
本文代碼截取自實(shí)際項(xiàng)目。
jwt(Json Web Token),一個(gè)token,令牌。
簡(jiǎn)單流程:
用戶登錄成功后,后端返回一個(gè)token,也就是頒發(fā)給用戶一個(gè)憑證。之后每一次訪問,前端都需要攜帶這個(gè)token,后端通過token來解析出當(dāng)前訪問對(duì)象。
優(yōu)點(diǎn)
1、一定程度上解放了后端,后端不需要再記錄當(dāng)前用戶是誰,不需要再維護(hù)一個(gè)session,節(jié)省了開銷。
2、session依賴于cookie,某些場(chǎng)合cookie是用不了的,比如用戶瀏覽器cookie被禁用、移動(dòng)端無法存儲(chǔ)cookie等。
缺點(diǎn)
1、既然服務(wù)器通過token就可以知道當(dāng)前用戶是誰,說明其中包含了用戶信息,有一定的泄露風(fēng)險(xiǎn)。
2、因?yàn)槭菬o狀態(tài)的,服務(wù)器不維持會(huì)話,那么每一次請(qǐng)求都會(huì)重新去數(shù)據(jù)庫讀取權(quán)限信息,性能有一定影響。
(如果想提高性能,可以將權(quán)限數(shù)據(jù)存到redis,但是如果用redis,就已經(jīng)失去了jwt的優(yōu)點(diǎn),直接用普通的token+redis即可)
3、只能校驗(yàn)token是否正確,通過設(shè)定過期時(shí)間來確定其有效性,不可以手動(dòng)注銷。
先說怎么做,再說為什么。
代碼
依賴
<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"; /** * 過期時(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(); } }
過濾器
一個(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ù),我這邊就不貼出來了。
流程分析
總體分析
代碼肯定不是憑空出來的,必定有章可循。
首先看網(wǎng)上找的這個(gè)圖:
最前面兩個(gè)過濾器:密碼檢驗(yàn)、權(quán)限認(rèn)證。(順序很重要,鑒權(quán)之前總得給人家輸入賬號(hào)密碼的機(jī)會(huì)吧)
想一下傳統(tǒng)的session會(huì)話模式,登錄成功后,服務(wù)器維護(hù)了session,瀏覽器得cookie里存放了sessionId,用戶訪問的時(shí)候?yàn)g覽器自動(dòng)帶上sessionId。
放在以前,服務(wù)器端的工作都是是框架幫我們做了(原生session來自于tomcat)。
換成jwt后,需要前端自己去存儲(chǔ)token,后臺(tái)自己來解析token。
但是流程還是一樣的,用戶登錄、獲取token、攜帶token、校驗(yàn)token。。。。。。
登錄校驗(yàn)過濾器分析
分析完流程后,就需要看看,如果不用jwt,框架自身是如何實(shí)現(xiàn)的。
找到UsernamePasswordAuthenticationFilter這個(gè)類
可以發(fā)現(xiàn)這個(gè)類主要是用來檢驗(yàn)密碼生成,憑證的。那我們只要繼承這個(gè)UsernamePasswordAuthenticationFilter類,重寫attemptAuthentication,就可以實(shí)現(xiàn)登錄校驗(yàn)功能。
所以我們的代碼是這樣的:
權(quán)限認(rèn)證過濾器分析
需要看一下BasicAuthenticationFilter的源碼
其中的關(guān)鍵方法:
思考一下,我們?nèi)绻枰脩粜畔?,?huì)從哪里取?
自然是請(qǐng)求頭,前端訪問的時(shí)候,會(huì)把token放在請(qǐng)求頭。
取得token后,就要開始解析token了。token正確,將認(rèn)證信息以及權(quán)限信息注入,token不正確則可以拋出異常。這就是我們第二個(gè)過濾器的由來
一個(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 如何通過JWTtoken進(jìn)行認(rèn)證授權(quán)
- springSecurity自定義登錄接口和JWT認(rèn)證過濾器的流程
- 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ù)庫登錄認(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基于令牌的無狀態(tài)認(rèn)證實(shí)現(xiàn)
相關(guān)文章
Java實(shí)現(xiàn)企業(yè)發(fā)放的獎(jiǎng)金根據(jù)利潤(rùn)提成問題
這篇文章主要介紹了請(qǐng)利用數(shù)軸來分界,定位。注意定義時(shí)需把獎(jiǎng)金定義成長(zhǎng)整型,需要的朋友可以參考下2017-02-02Eclipse 出現(xiàn)Failed to load JavaHL Library解決方法
這篇文章主要介紹了Eclipse 出現(xiàn)Failed to load JavaHL Library解決方法的相關(guān)資料,今天使用Eclipse 時(shí)出現(xiàn)以上錯(cuò)誤,本文說明如何更更正,需要的朋友可以參考下2016-11-11子線程任務(wù)發(fā)生異常時(shí)主線程事務(wù)回滾示例過程
這篇文章主要為大家介紹了子線程任務(wù)發(fā)生了異常時(shí)主線程事務(wù)如何回滾的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03Spring Boot 中的自動(dòng)配置autoconfigure詳解
這篇文章主要介紹了Spring Boot 中的自動(dòng)配置autoconfigure詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01IDEA中配置Python環(huán)境并運(yùn)行方式
本文介紹了在Mac和Windows平臺(tái)上安裝Python環(huán)境的方法,并詳細(xì)講解了如何在IntelliJ IDEA中安裝Python插件、創(chuàng)建Python工程和運(yùn)行Python文件,同時(shí),還提到了一些常用的Python框架,如Django、Google App Engine和SQL支持2025-03-03Spring中BeanFactory與FactoryBean接口的區(qū)別詳解
這篇文章主要給大家介紹了關(guān)于Spring中BeanFactory與FactoryBean接口的區(qū)別的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Spring具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Java8新的異步編程方式CompletableFuture實(shí)現(xiàn)
這篇文章主要介紹了Java8新的異步編程方式CompletableFuture實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-04-04