SpringBoot整合SpringSecurity實(shí)現(xiàn)JWT認(rèn)證的項(xiàng)目實(shí)踐
前言
微服務(wù)架構(gòu),前后端分離目前已成為互聯(lián)網(wǎng)項(xiàng)目開發(fā)的業(yè)界標(biāo)準(zhǔn),其核心思想就是前端(APP、小程序、H5頁(yè)面等)通過調(diào)用后端的API接口,提交及返回JSON數(shù)據(jù)進(jìn)行交互。
在前后端分離項(xiàng)目中,首先要解決的就是登錄及授權(quán)的問題。微服務(wù)架構(gòu)下,傳統(tǒng)的session認(rèn)證限制了應(yīng)用的擴(kuò)展能力,無(wú)狀態(tài)的JWT認(rèn)證方法應(yīng)運(yùn)而生,該認(rèn)證機(jī)制特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景
1、創(chuàng)建SpringBoot工程
2、導(dǎo)入SpringSecurity與JWT的相關(guān)依賴
pom文件加入以下依賴
<!--Security框架--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ... <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.6</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.6</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.6</version> </dependency>
3.定義SpringSecurity需要的基礎(chǔ)處理類
application.yml配置中加入jwt配置信息:
#jwt jwt: header: Authorization # 令牌前綴 token-start-with: Bearer # 使用Base64對(duì)該令牌進(jìn)行編碼 base64-secret: XXXXXXXXXXXXXXXX(制定您的密鑰) # 令牌過期時(shí)間 此處單位/毫秒 token-validity-in-seconds: 14400000
創(chuàng)建一個(gè)jwt的配置類,并注入Spring,便于程序中靈活調(diào)用
@Data @Configuration @ConfigurationProperties(prefix = "jwt") public class JwtSecurityProperties { /** Request Headers : Authorization */ private String header; /** 令牌前綴,最后留個(gè)空格 Bearer */ private String tokenStartWith; /** Base64對(duì)該令牌進(jìn)行編碼 */ private String base64Secret; /** 令牌過期時(shí)間 此處單位/毫秒 */ private Long tokenValidityInSeconds; /**返回令牌前綴 */ public String getTokenStartWith() { return tokenStartWith + " "; } }
定義無(wú)權(quán)限訪問類
@Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } }
定義認(rèn)證失敗處理類
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage()); } }
4. 構(gòu)建JWT token工具類
工具類實(shí)現(xiàn)創(chuàng)建token與校驗(yàn)token功能
@Slf4j @Component public class JwtTokenUtils implements InitializingBean { private final JwtSecurityProperties jwtSecurityProperties; private static final String AUTHORITIES_KEY = "auth"; private Key key; public JwtTokenUtils(JwtSecurityProperties jwtSecurityProperties) { this.jwtSecurityProperties = jwtSecurityProperties; } @Override public void afterPropertiesSet() { byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperties.getBase64Secret()); this.key = Keys.hmacShaKeyFor(keyBytes); } public String createToken (Map<String, Object> claims) { return Jwts.builder() .claim(AUTHORITIES_KEY, claims) .setId(UUID.randomUUID().toString()) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + jwtSecurityProperties.getTokenValidityInSeconds())) .compressWith(CompressionCodecs.DEFLATE) .signWith(key,SignatureAlgorithm.HS512) .compact(); } public Date getExpirationDateFromToken(String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null; } return expiration; } public Authentication getAuthentication(String token) { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); HashMap map =(HashMap) claims.get("auth"); User principal = new User(map.get("user").toString(), map.get("password").toString(), authorities); return new UsernamePasswordAuthenticationToken(principal, token, authorities); } public boolean validateToken(String authToken) { try { Jwts.parser().setSigningKey(key).parseClaimsJws(authToken); return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { log.info("Invalid JWT signature."); e.printStackTrace(); } catch (ExpiredJwtException e) { log.info("Expired JWT token."); e.printStackTrace(); } catch (UnsupportedJwtException e) { log.info("Unsupported JWT token."); e.printStackTrace(); } catch (IllegalArgumentException e) { log.info("JWT token compact of handler are invalid."); e.printStackTrace(); } return false; } private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null; } return claims; } }
5.實(shí)現(xiàn)token驗(yàn)證的過濾器
該類繼承OncePerRequestFilter,顧名思義,它能夠確保在一次請(qǐng)求中只通過一次filter
該類使用JwtTokenUtils工具類進(jìn)行token校驗(yàn)
@Component @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private JwtTokenUtils jwtTokenUtils; public JwtAuthenticationTokenFilter(JwtTokenUtils jwtTokenUtils) { this.jwtTokenUtils = jwtTokenUtils; } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { JwtSecurityProperties jwtSecurityProperties = SpringContextHolder.getBean(JwtSecurityProperties.class); String requestRri = httpServletRequest.getRequestURI(); //獲取request token String token = null; String bearerToken = httpServletRequest.getHeader(jwtSecurityProperties.getHeader()); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtSecurityProperties.getTokenStartWith())) { token = bearerToken.substring(jwtSecurityProperties.getTokenStartWith().length()); } if (StringUtils.hasText(token) && jwtTokenUtils.validateToken(token)) { Authentication authentication = jwtTokenUtils.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestRri); } else { log.debug("no valid JWT token found, uri: {}", requestRri); } filterChain.doFilter(httpServletRequest, httpServletResponse); } }
根據(jù)SpringBoot官方讓重復(fù)執(zhí)行的filter實(shí)現(xiàn)一次執(zhí)行過程的解決方案,參見官網(wǎng)地址:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter
在SpringBoot啟動(dòng)類中,加入以下代碼:
@Bean public FilterRegistrationBean registration(JwtAuthenticationTokenFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(filter); registration.setEnabled(false); return registration; }
6. SpringSecurity的關(guān)鍵配置
SpringBoot推薦使用配置類來(lái)代替xml配置,該類中涉及了以上幾個(gè)bean來(lái)供security使用
- JwtAccessDeniedHandler :無(wú)權(quán)限訪問
- jwtAuthenticationEntryPoint :認(rèn)證失敗處理
- jwtAuthenticationTokenFilter :token驗(yàn)證的過濾器
package com.zhuhuix.startup.security.config; import com.fasterxml.jackson.core.filter.TokenFilter; import com.zhuhuix.startup.security.security.JwtAccessDeniedHandler; import com.zhuhuix.startup.security.security.JwtAuthenticationEntryPoint; import com.zhuhuix.startup.security.security.JwtAuthenticationTokenFilter; import com.zhuhuix.startup.security.utils.JwtTokenUtils; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * Spring Security配置類 * * @author zhuhuix * @date 2020-03-25 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final JwtAccessDeniedHandler jwtAccessDeniedHandler; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtTokenUtils jwtTokenUtils; public WebSecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtTokenUtils jwtTokenUtils) { this.jwtAccessDeniedHandler = jwtAccessDeniedHandler; this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; this.jwtTokenUtils = jwtTokenUtils; } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // 禁用 CSRF .csrf().disable() // 授權(quán)異常 .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) // 防止iframe 造成跨域 .and() .headers() .frameOptions() .disable() // 不創(chuàng)建會(huì)話 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 放行靜態(tài)資源 .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/webSocket/**" ).permitAll() // 放行swagger .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/webjars/**").permitAll() .antMatchers("/*/api-docs").permitAll() // 放行文件訪問 .antMatchers("/file/**").permitAll() // 放行druid .antMatchers("/druid/**").permitAll() // 放行OPTIONS請(qǐng)求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() //允許匿名及登錄用戶訪問 .antMatchers("/api/auth/**", "/error/**").permitAll() // 所有請(qǐng)求都需要認(rèn)證 .anyRequest().authenticated(); // 禁用緩存 httpSecurity.headers().cacheControl(); // 添加JWT filter httpSecurity .apply(new TokenConfigurer(jwtTokenUtils)); } public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private final JwtTokenUtils jwtTokenUtils; public TokenConfigurer(JwtTokenUtils jwtTokenUtils){ this.jwtTokenUtils = jwtTokenUtils; } @Override public void configure(HttpSecurity http) { JwtAuthenticationTokenFilter customFilter = new JwtAuthenticationTokenFilter(jwtTokenUtils); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } } }
7. 編寫Controller進(jìn)行測(cè)試
登錄邏輯:傳遞user與password參數(shù),返回token
@Slf4j @RestController @RequestMapping("/api/auth") @Api(tags = "系統(tǒng)授權(quán)接口") public class AuthController { private final JwtTokenUtils jwtTokenUtils; public AuthController(JwtTokenUtils jwtTokenUtils) { this.jwtTokenUtils = jwtTokenUtils; } @ApiOperation("登錄授權(quán)") @GetMapping(value = "/login") public String login(String user,String password){ Map map = new HashMap(); map.put("user",user); map.put("password",password); return jwtTokenUtils.createToken(map); } }
使用IDEA Rest Client測(cè)試如下:
驗(yàn)證邏輯:傳遞token,驗(yàn)證成功后返回用戶信息
token驗(yàn)證錯(cuò)誤返回401:
到此這篇關(guān)于SpringBoot整合SpringSecurity實(shí)現(xiàn)JWT認(rèn)證的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity JWT認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 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ù)庫(kù)登錄認(rèn)證的實(shí)現(xiàn)
- SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringSecurity整合jwt權(quán)限認(rèn)證的全流程講解
- SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實(shí)現(xiàn)
- SpringSecurity JWT基于令牌的無(wú)狀態(tài)認(rèn)證實(shí)現(xiàn)
相關(guān)文章
Spring?Boot?如何生成微信小程序短連接及發(fā)送短信在短信中打開小程序操作
最近遇到這樣的需求需要發(fā)送短信,通過短信中的短連接打開小程序操作,下面小編給大家分享Spring?Boot?如何生成微信小程序短連接發(fā)送短信在短信中打開小程序操作,感興趣的朋友跟隨小編一起看看吧2024-03-03@Valid 無(wú)法校驗(yàn)List<E>的問題
這篇文章主要介紹了@Valid 無(wú)法校驗(yàn)List<E>的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Java?SM2加密相關(guān)實(shí)現(xiàn)與簡(jiǎn)單原理詳解
SM2算法可以用較少的計(jì)算能力提供比RSA算法更高的安全強(qiáng)度,而所需的密鑰長(zhǎng)度卻遠(yuǎn)比RSA算法低,這篇文章主要給大家介紹了關(guān)于Java?SM2加密相關(guān)實(shí)現(xiàn)與簡(jiǎn)單原理的相關(guān)資料,需要的朋友可以參考下2024-01-01淺談Java內(nèi)存模型之happens-before
于存在線程本地內(nèi)存和主內(nèi)存的原因,再加上重排序,會(huì)導(dǎo)致多線程環(huán)境下存在可見性的問題。那么我們正確使用同步、鎖的情況下,線程A修改了變量a何時(shí)對(duì)線程B可見?下面小編來(lái)簡(jiǎn)單介紹下2019-05-05