SpringBoot?SpringSecurity?JWT實(shí)現(xiàn)系統(tǒng)安全策略詳解
security進(jìn)行用戶驗(yàn)證和授權(quán);jwt負(fù)責(zé)頒發(fā)令牌與校驗(yàn),判斷用戶登錄狀態(tài)
一、原理
1. SpringSecurity過濾器鏈
SpringSecurity 采用的是責(zé)任鏈的設(shè)計(jì)模式,它有一條很長的過濾器鏈。
- SecurityContextPersistenceFilter:在每次請求處理之前將該請求相關(guān)的安全上下文信息加載到 SecurityContextHolder 中。
- LogoutFilter:用于處理退出登錄。
- UsernamePasswordAuthenticationFilter:用于處理基于表單的登錄請求,從表單中獲取用戶名和密碼。
- BasicAuthenticationFilter:檢測和處理 http basic 認(rèn)證。
- ExceptionTranslationFilter:處理 AccessDeniedException 和 AuthenticationException 異常。
- FilterSecurityInterceptor:可以看做過濾器鏈的出口。
- …
流程說明:客戶端發(fā)起一個(gè)請求,進(jìn)入 Security 過濾器鏈。
當(dāng)?shù)?LogoutFilter 的時(shí)候判斷是否是登出路徑,如果是登出路徑則到 logoutHandler ,如果登出成功則到 logoutSuccessHandler 登出成功處理,如果登出失敗則由 ExceptionTranslationFilter ;如果不是登出路徑則直接進(jìn)入下一個(gè)過濾器。
當(dāng)?shù)?UsernamePasswordAuthenticationFilter 的時(shí)候判斷是否為登錄路徑,如果是,則進(jìn)入該過濾器進(jìn)行登錄操作,如果登錄失敗則到 AuthenticationFailureHandler 登錄失敗處理器處理,如果登錄成功則到 AuthenticationSuccessHandler 登錄成功處理器處理,如果不是登錄請求則不進(jìn)入該過濾器。
當(dāng)?shù)?FilterSecurityInterceptor 的時(shí)候會(huì)拿到 uri ,根據(jù) uri 去找對應(yīng)的鑒權(quán)管理器,鑒權(quán)管理器做鑒權(quán)工作,鑒權(quán)成功則到 Controller 層否則到 AccessDeniedHandler 鑒權(quán)失敗處理器處理。
2. JWT校驗(yàn)
首先前端一樣是把登錄信息發(fā)送給后端,后端查詢數(shù)據(jù)庫校驗(yàn)用戶的賬號和密碼是否正確,正確的話則使用jwt生成token,并且返回給前端。以后前端每次請求時(shí),都需要攜帶token,后端獲取token后,使用jwt進(jìn)行驗(yàn)證用戶的token是否無效或過期,驗(yàn)證成功后才去做相應(yīng)的邏輯。
二、Security+JWT配置說明
1. 添加maven依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
2. securityConfig配置
/** * Security 配置 */ @Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired LoginFailureHandler loginFailureHandler; @Autowired LoginSuccessHandler loginSuccessHandler; @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired JwtAccessDeniedHandler jwtAccessDeniedHandler; @Autowired UserDetailServiceImpl userDetailService; @Autowired JWTLogoutSuccessHandler jwtLogoutSuccessHandler; @Autowired CaptchaFilter captchaFilter; @Value("${security.enable}") private Boolean securityIs = Boolean.TRUE; @Value("${security.permit}") private String permit; @Bean public HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); //此處可添加別的規(guī)則,目前只設(shè)置 允許雙 // firewall.setAllowUrlEncodedDoubleSlash(true); return firewall; } @Bean JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager(), jwtAuthenticationEntryPoint); return jwtAuthenticationFilter; } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.cors().and().csrf().disable() // 登錄配置 .formLogin() .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) .and() .logout() .logoutSuccessHandler(jwtLogoutSuccessHandler) // 禁用session .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 配置攔截規(guī)則 .and() .authorizeRequests() .antMatchers(permit.split(",")).permitAll(); if (!securityIs) { http.authorizeRequests().antMatchers("/**").permitAll(); } registry.anyRequest().authenticated() // 異常處理器 .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) // 配置自定義的過濾器 .and() .addFilter(jwtAuthenticationFilter()) // 驗(yàn)證碼過濾器放在UsernamePassword過濾器之前 .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService); } }
3. JwtAuthenticationFilter校驗(yàn)token
package cn.piesat.gf.filter; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import cn.piesat.gf.dao.user.SysUserDao; import cn.piesat.gf.model.entity.user.SysUser; import cn.piesat.gf.exception.ExpiredAuthenticationException; import cn.piesat.gf.exception.MyAuthenticationException; import cn.piesat.gf.service.user.impl.UserDetailServiceImpl; import cn.piesat.gf.utils.Constants; import cn.piesat.gf.utils.JwtUtils; import cn.piesat.gf.utils.Result; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; @Slf4j public class JwtAuthenticationFilter extends BasicAuthenticationFilter { private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationManager authenticationManager; @Autowired JwtUtils jwtUtils; @Autowired UserDetailServiceImpl userDetailService; @Autowired SysUserDao sysUserRepository; @Autowired RedisTemplate redisTemplate; @Value("${security.single}") private Boolean singleLogin = false; public JwtAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) { super(authenticationManager, authenticationEntryPoint); Assert.notNull(authenticationManager, "authenticationManager cannot be null"); Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); this.authenticationManager = authenticationManager; this.authenticationEntryPoint = authenticationEntryPoint; } public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String jwt = request.getHeader(jwtUtils.getHeader()); // 這里如果沒有jwt,繼續(xù)往后走,因?yàn)楹竺孢€有鑒權(quán)管理器等去判斷是否擁有身份憑證,所以是可以放行的 // 沒有jwt相當(dāng)于匿名訪問,若有一些接口是需要權(quán)限的,則不能訪問這些接口 if (StrUtil.isBlankOrUndefined(jwt)) { chain.doFilter(request, response); return; } try { Claims claim = jwtUtils.getClaimsByToken(jwt); if (claim == null) { throw new MyAuthenticationException("token 異常"); } if (jwtUtils.isTokenExpired(claim)) { throw new MyAuthenticationException("token 已過期"); } String username = claim.getSubject(); Object o1 = redisTemplate.opsForValue().get(Constants.TOKEN_KEY + username); String o = null; if(!ObjectUtils.isEmpty(o1)){ o = o1.toString(); } if (!StringUtils.hasText(o)) { throw new ExpiredAuthenticationException("您的登錄信息已過期,請重新登錄!"); } if (singleLogin && StringUtils.hasText(o) && !jwt.equals(o)) { throw new MyAuthenticationException("您的賬號已別處登錄,您已下線,如有異常請及時(shí)修改密碼!"); } // 獲取用戶的權(quán)限等信息 SysUser sysUser = sysUserRepository.findByUserName(username); // 構(gòu)建UsernamePasswordAuthenticationToken,這里密碼為null,是因?yàn)樘峁┝苏_的JWT,實(shí)現(xiàn)自動(dòng)登錄 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getUserId())); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } catch (AuthenticationException e) { log.error(ExceptionUtil.stacktraceToString(e)); authenticationEntryPoint.commence(request, response, e); return; } catch (Exception e){ log.error(ExceptionUtil.stacktraceToString(e)); response.getOutputStream().write(JSONUtil.toJsonStr(Result.fail(e.getMessage())).getBytes(StandardCharsets.UTF_8)); response.getOutputStream().flush(); response.getOutputStream().close(); return; } } }
4. JWT生成與解析工具類
package cn.piesat.gf.utils; import cn.hutool.core.exceptions.ExceptionUtil; 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.stereotype.Component; import java.util.Date; @Data @Component @ConfigurationProperties(prefix = "jwt.config") @Slf4j public class JwtUtils { private long expire; private String secret; private String header; // 生成JWT public String generateToken(String username) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + 1000 * expire); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(username) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // 解析JWT public Claims getClaimsByToken(String jwt) { try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(jwt) .getBody(); } catch (Exception e) { log.error(ExceptionUtil.stacktraceToString(e)); return null; } } // 判斷JWT是否過期 public boolean isTokenExpired(Claims claims) { return claims.getExpiration().before(new Date()); } }
到此這篇關(guān)于SpringBoot SpringSecurity JWT實(shí)現(xiàn)系統(tǒng)安全策略詳解的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity JWT內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
- SpringBoot整合SpringSecurity實(shí)現(xiàn)JWT認(rèn)證的項(xiàng)目實(shí)踐
- springboot+jwt+springSecurity微信小程序授權(quán)登錄問題
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
相關(guān)文章
SpringBoot中的@EnableConfigurationProperties注解詳細(xì)解析
這篇文章主要介紹了SpringBoot中的@EnableConfigurationProperties注解詳細(xì)解析,如果一個(gè)配置類只配置@ConfigurationProperties注解,而沒有使用@Component或者實(shí)現(xiàn)了@Component的其他注解,那么在IOC容器中是獲取不到properties 配置文件轉(zhuǎn)化的bean,需要的朋友可以參考下2024-01-01基于Java多線程notify與notifyall的區(qū)別分析
本篇文章對Java中多線程notify與notifyall的區(qū)別進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05spring?data?jpa查詢一個(gè)實(shí)體類的部分屬性方式
這篇文章主要介紹了spring?data?jpa查詢一個(gè)實(shí)體類的部分屬性方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02解決java.sql.SQLException:?validateConnection?false問題的方法匯總(最
這篇文章主要給大家介紹了關(guān)于解決java.sql.SQLException:?validateConnection?false問題的方法匯總,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03Java中StringUtils工具類的一些用法實(shí)例
這篇文章主要介紹了Java中StringUtils工具類的一些用法實(shí)例,本文著重講解了isEmpty和isBlank方法的使用,另外也講解了trim、strip等方法的使用實(shí)例,需要的朋友可以參考下2015-06-06Mybatis?plus多租戶方案的實(shí)戰(zhàn)踩坑記錄
MybaitsPlus多租戶處理器是一個(gè)對于多租戶問題的解決方案,下面這篇文章主要給大家介紹了關(guān)于Mybatis?plus多租戶方案踩坑的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-12-12Java 使用Docker時(shí)經(jīng)常遇到的五個(gè)問題
這篇文章主要介紹了Java 使用Docker時(shí)經(jīng)常遇到的五個(gè)問題的相關(guān)資料,需要的朋友可以參考下2016-10-10