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