SpringBoot整合SpringSecurity實現(xiàn)JWT認(rèn)證的項目實踐
前言
微服務(wù)架構(gòu),前后端分離目前已成為互聯(lián)網(wǎng)項目開發(fā)的業(yè)界標(biāo)準(zhǔn),其核心思想就是前端(APP、小程序、H5頁面等)通過調(diào)用后端的API接口,提交及返回JSON數(shù)據(jù)進行交互。
在前后端分離項目中,首先要解決的就是登錄及授權(quán)的問題。微服務(wù)架構(gòu)下,傳統(tǒng)的session認(rèn)證限制了應(yīng)用的擴展能力,無狀態(tài)的JWT認(rèn)證方法應(yīng)運而生,該認(rèn)證機制特別適用于分布式站點的單點登錄(SSO)場景
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對該令牌進行編碼 base64-secret: XXXXXXXXXXXXXXXX(制定您的密鑰) # 令牌過期時間 此處單位/毫秒 token-validity-in-seconds: 14400000
創(chuàng)建一個jwt的配置類,并注入Spring,便于程序中靈活調(diào)用
@Data @Configuration @ConfigurationProperties(prefix = "jwt") public class JwtSecurityProperties { /** Request Headers : Authorization */ private String header; /** 令牌前綴,最后留個空格 Bearer */ private String tokenStartWith; /** Base64對該令牌進行編碼 */ private String base64Secret; /** 令牌過期時間 此處單位/毫秒 */ private Long tokenValidityInSeconds; /**返回令牌前綴 */ public String getTokenStartWith() { return tokenStartWith + " "; } }
定義無權(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工具類
工具類實現(xiàn)創(chuàng)建token與校驗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.實現(xiàn)token驗證的過濾器
該類繼承OncePerRequestFilter,顧名思義,它能夠確保在一次請求中只通過一次filter
該類使用JwtTokenUtils工具類進行token校驗
@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實現(xiàn)一次執(zhí)行過程的解決方案,參見官網(wǎng)地址:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter
在SpringBoot啟動類中,加入以下代碼:
@Bean public FilterRegistrationBean registration(JwtAuthenticationTokenFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(filter); registration.setEnabled(false); return registration; }
6. SpringSecurity的關(guān)鍵配置
SpringBoot推薦使用配置類來代替xml配置,該類中涉及了以上幾個bean來供security使用
- JwtAccessDeniedHandler :無權(quán)限訪問
- jwtAuthenticationEntryPoint :認(rèn)證失敗處理
- jwtAuthenticationTokenFilter :token驗證的過濾器
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)建會話 .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請求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() //允許匿名及登錄用戶訪問 .antMatchers("/api/auth/**", "/error/**").permitAll() // 所有請求都需要認(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進行測試
登錄邏輯:傳遞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測試如下:
驗證邏輯:傳遞token,驗證成功后返回用戶信息
token驗證錯誤返回401:
到此這篇關(guān)于SpringBoot整合SpringSecurity實現(xiàn)JWT認(rèn)證的項目實踐的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity JWT認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 詳解Spring Boot實戰(zhàn)之Filter實現(xiàn)使用JWT進行接口認(rèn)證
- Vue+Jwt+SpringBoot+Ldap完成登錄認(rèn)證的示例代碼
- Springboot集成Spring Security實現(xiàn)JWT認(rèn)證的步驟詳解
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot使用Jwt處理跨域認(rèn)證問題的教程詳解
- 詳解SpringBoot如何使用JWT實現(xiàn)身份認(rèn)證和授權(quán)
- 利用Springboot實現(xiàn)Jwt認(rèn)證的示例代碼
- springboot+jwt實現(xiàn)token登陸權(quán)限認(rèn)證的實現(xiàn)
- Spring Boot整合JWT實現(xiàn)認(rèn)證與授權(quán)的項目實踐
相關(guān)文章
Java如何根據(jù)不同系統(tǒng)動態(tài)獲取換行符和盤分割符
這篇文章主要介紹了Java如何根據(jù)不同系統(tǒng)動態(tài)獲取換行符和盤分割符,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12Java高效實現(xiàn)excel轉(zhuǎn)pdf(支持帶圖片的轉(zhuǎn)換)
這篇文章主要為大家詳細(xì)介紹了如何用java實現(xiàn)excel轉(zhuǎn)pdf文件,并且支持excel單元格中帶有圖片的轉(zhuǎn)換,文中的示例代碼講解詳細(xì),需要的可以參考下2024-01-01Java中OAuth2.0第三方授權(quán)原理與實戰(zhàn)
本文主要介紹了Java中OAuth2.0第三方授權(quán)原理與實戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05SpringBoot中攔截器和動態(tài)代理的區(qū)別詳解
在?Spring?Boot?中,攔截器和動態(tài)代理都是用來實現(xiàn)功能增強的,所以在很多時候,有人會認(rèn)為攔截器的底層是通過動態(tài)代理實現(xiàn)的,所以本文就來盤點一下他們兩的區(qū)別,以及攔截器的底層實現(xiàn)吧2023-09-09