Springboot集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的步驟詳解
1 簡(jiǎn)介
Spring Security作為成熟且強(qiáng)大的安全框架,得到許多大廠的青睞。而作為前后端分離的SSO方案,JWT也在許多項(xiàng)目中應(yīng)用。本文將介紹如何通過Spring Security實(shí)現(xiàn)JWT認(rèn)證。
用戶與服務(wù)器交互大概如下:
- 客戶端獲取JWT,一般通過POST方法把用戶名/密碼傳給server;
- 服務(wù)端接收到客戶端的請(qǐng)求后,會(huì)檢驗(yàn)用戶名/密碼是否正確,如果正確則生成JWT并返回;不正確則返回錯(cuò)誤;
- 客戶端拿到JWT后,在有效期內(nèi)都可以通過JWT來訪問資源了,一般把JWT放在請(qǐng)求頭;一次獲取,多次使用;
- 服務(wù)端校驗(yàn)JWT是否合法,合法則允許客戶端正常訪問,不合法則返回401。
2 項(xiàng)目整合
我們把要整合的Spring Security和JWT加入到項(xiàng)目的依賴中去:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <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.1 JWT整合
2.1.1 JWT工具類
JWT工具類起碼要具有以下功能:
- 根據(jù)用戶信息生成JWT;
- 校驗(yàn)JWT是否合法,如是否被篡改、是否過期等;
- 從JWT中解析用戶信息,如用戶名、權(quán)限等;
具體代碼如下:
@Component public class JwtTokenProvider { @Autowired JwtProperties jwtProperties; @Autowired private CustomUserDetailsService userDetailsService; private String secretKey; @PostConstruct protected void init() { secretKey = Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes()); } public String createToken(String username, List<String> roles) { Claims claims = Jwts.claims().setSubject(username); claims.put("roles", roles); Date now = new Date(); Date validity = new Date(now.getTime() + jwtProperties.getValidityInMs()); return Jwts.builder()// .setClaims(claims)// .setIssuedAt(now)// .setExpiration(validity)// .signWith(SignatureAlgorithm.HS256, secretKey)// .compact(); } public Authentication getAuthentication(String token) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } public String getUsername(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } public String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } public boolean validateToken(String token) { try { Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); if (claims.getBody().getExpiration().before(new Date())) { return false; } return true; } catch (JwtException | IllegalArgumentException e) { throw new InvalidJwtAuthenticationException("Expired or invalid JWT token"); } } }
工具類還實(shí)現(xiàn)了另一個(gè)功能:從HTTP請(qǐng)求頭中獲取JWT。
2.1.2 Token處理的Filter
Filter是Security處理的關(guān)鍵,基本上都是通過Filter來攔截請(qǐng)求的。首先從請(qǐng)求頭取出JWT,然后校驗(yàn)JWT是否合法,如果合法則取出Authentication保存在SecurityContextHolder里。如果不合法,則做異常處理。
public class JwtTokenAuthenticationFilter extends GenericFilterBean { private JwtTokenProvider jwtTokenProvider; public JwtTokenAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { String token = jwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { Authentication auth = jwtTokenProvider.getAuthentication(token); if (auth != null) { SecurityContextHolder.getContext().setAuthentication(auth); } } } catch (InvalidJwtAuthenticationException e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("Invalid token"); response.getWriter().flush(); return; } filterChain.doFilter(req, res); } }
對(duì)于異常處理,使用@ControllerAdvice是不行的,應(yīng)該這個(gè)是Filter,在這里拋的異常還沒有到DispatcherServlet,無法處理。所以Filter要自己做異常處理:
catch (InvalidJwtAuthenticationException e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("Invalid token"); response.getWriter().flush(); return; }
最后的return不能省略,因?yàn)橐呀?jīng)要把輸出的內(nèi)容給Response了,沒有必要再往后傳遞,否則報(bào)錯(cuò)
java.lang.IllegalStateException: getWriter() has already been called
2.1.3 JWT屬性
JWT需要配置一個(gè)密鑰來加密,同時(shí)還要配置JWT令牌的有效期。
@Configuration @ConfigurationProperties(prefix = "pkslow.jwt") public class JwtProperties { private String secretKey = "pkslow.key"; private long validityInMs = 3600_000; //getter and setter }
2.2 Spring Security整合
Spring Security的整個(gè)框架還是比較復(fù)雜的,簡(jiǎn)化后大概如下圖所示:
它是通過一連串的Filter來進(jìn)行安全管理。細(xì)節(jié)這里先不展開講。
2.2.1 WebSecurityConfigurerAdapter配置
這個(gè)配置也可以理解為是FilterChain的配置,可以不用理解,代碼很好懂它做了什么:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtTokenProvider jwtTokenProvider; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(HttpSecurity http) throws Exception { http .httpBasic().disable() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/auth/login").permitAll() .antMatchers(HttpMethod.GET, "/admin").hasRole("ADMIN") .antMatchers(HttpMethod.GET, "/user").hasRole("USER") .anyRequest().authenticated() .and() .apply(new JwtSecurityConfigurer(jwtTokenProvider)); } }
這里通過HttpSecurity配置了哪些請(qǐng)求需要什么權(quán)限才可以訪問。
- /auth/login用于登陸獲取JWT,所以都能訪問;
- /admin只有ADMIN用戶才可以訪問;
- /user只有USER用戶才可以訪問。
而之前實(shí)現(xiàn)的Filter則在下面配置使用:
public class JwtSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private JwtTokenProvider jwtTokenProvider; public JwtSecurityConfigurer(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } @Override public void configure(HttpSecurity http) throws Exception { JwtTokenAuthenticationFilter customFilter = new JwtTokenAuthenticationFilter(jwtTokenProvider); http.exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) .and() .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }
2.2.2 用戶從哪來
通常在Spring Security的世界里,都是通過實(shí)現(xiàn)UserDetailsService來獲取UserDetails的。
@Component public class CustomUserDetailsService implements UserDetailsService { private UserRepository users; public CustomUserDetailsService(UserRepository users) { this.users = users; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return this.users.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found")); } }
對(duì)于UserRepository
,可以從數(shù)據(jù)庫(kù)中讀取,或者其它用戶管理中心。為了方便,我使用Map放了兩個(gè)用戶:
@Repository public class UserRepository { private static final Map<String, User> allUsers = new HashMap<>(); @Autowired private PasswordEncoder passwordEncoder; @PostConstruct protected void init() { allUsers.put("pkslow", new User("pkslow", passwordEncoder.encode("123456"), Collections.singletonList("ROLE_ADMIN"))); allUsers.put("user", new User("user", passwordEncoder.encode("123456"), Collections.singletonList("ROLE_USER"))); } public Optional<User> findByUsername(String username) { return Optional.ofNullable(allUsers.get(username)); } }
3 測(cè)試
完成代碼編寫后,我們來測(cè)試一下:
(1)無JWT訪問,失敗
curl http://localhost:8080/admin {"timestamp":"2021-02-06T05:45:06.385+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/admin"} $ curl http://localhost:8080/user {"timestamp":"2021-02-06T05:45:16.438+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/user"}
(2)admin獲取JWT,密碼錯(cuò)誤則失敗,密碼正確則成功
$ curl http://localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"xxxxxx"}' -H 'Content-Type: application/json' {"timestamp":"2021-02-06T05:47:16.254+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/auth/login"} $ curl http://localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"123456"}' -H 'Content-Type: application/json' eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo
(3)admin帶JWT訪問/admin,成功;訪問/user失敗
$ curl http://localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo' you are admin $ curl http://localhost:8080/user -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo' {"timestamp":"2021-02-06T05:51:23.099+0000","status":403,"error":"Forbidden","message":"Forbidden","path":"/user"}
(4)使用過期的JWT訪問,失敗
$ curl http://localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDQ0OSwiZXhwIjoxNjEyNTkwNTA5fQ.CSaubE4iJcYATbLmbb59aNFU1jNCwDFHUV3zIakPU64' Invalid token
4 總結(jié)
代碼請(qǐng)查看:https://github.com/LarryDpk/pkslow-samples
以上就是Springboot集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的步驟詳解的詳細(xì)內(nèi)容,更多關(guān)于Springboot集成Spring Security的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 詳解Spring Boot實(shí)戰(zhàn)之Filter實(shí)現(xiàn)使用JWT進(jìn)行接口認(rèn)證
- Vue+Jwt+SpringBoot+Ldap完成登錄認(rèn)證的示例代碼
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot使用Jwt處理跨域認(rèn)證問題的教程詳解
- 詳解SpringBoot如何使用JWT實(shí)現(xiàn)身份認(rèn)證和授權(quán)
- 利用Springboot實(shí)現(xiàn)Jwt認(rèn)證的示例代碼
- springboot+jwt實(shí)現(xiàn)token登陸權(quán)限認(rèn)證的實(shí)現(xiàn)
- SpringBoot整合SpringSecurity實(shí)現(xiàn)JWT認(rèn)證的項(xiàng)目實(shí)踐
- Spring Boot整合JWT實(shí)現(xiàn)認(rèn)證與授權(quán)的項(xiàng)目實(shí)踐
相關(guān)文章
SpringBoot@Aspect 打印訪問請(qǐng)求和返回?cái)?shù)據(jù)方式
這篇文章主要介紹了SpringBoot@Aspect 打印訪問請(qǐng)求和返回?cái)?shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09IDEA2020.1同步系統(tǒng)設(shè)置到GitHub的方法
這篇文章主要介紹了IDEA2020.1同步系統(tǒng)設(shè)置到GitHub的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05Java阻塞隊(duì)列BlockingQueue基礎(chǔ)與使用
本文詳細(xì)介紹了BlockingQueue家庭中的所有成員,包括他們各自的功能以及常見使用場(chǎng)景,通過實(shí)例代碼介紹了Java 阻塞隊(duì)列BlockingQueue的相關(guān)知識(shí),需要的朋友可以參考下2023-01-01Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的長(zhǎng)輪詢的示例代碼
長(zhǎng)輪詢是與服務(wù)器保持即時(shí)通信的最簡(jiǎn)單的方式,它不使用任何特定的協(xié)議,例如 WebSocket ,所以也不依賴于瀏覽器版本等外部條件的兼容性。本文將用Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的長(zhǎng)輪詢,需要的可以參考一下2022-08-08Java并發(fā)編程中的CyclicBarrier使用解析
這篇文章主要介紹了Java并發(fā)編程中的CyclicBarrier使用解析,CyclicBarrier從字面意思上來看,循環(huán)柵欄,這篇文章就來分析下是到底是如何實(shí)現(xiàn)循環(huán)和柵欄的,需要的朋友可以參考下2023-12-12springboot整合日志處理Logback的實(shí)現(xiàn)示例
Logback是由log4j創(chuàng)始人設(shè)計(jì)的又一個(gè)開源日志組件,本文主要介紹了springboot整合日志處理Logback,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01使用Java實(shí)現(xiàn)5種負(fù)載均衡算法實(shí)例
負(fù)載均衡指由多臺(tái)服務(wù)器以對(duì)稱的方式組成一個(gè)服務(wù)器集合,每臺(tái)服務(wù)器都具有等價(jià)的地位,都可以單獨(dú)對(duì)外提供服務(wù)而無須其他服務(wù)器的輔助,這篇文章主要給大家介紹了關(guān)于使用Java實(shí)現(xiàn)5種負(fù)載均衡算法的相關(guān)資料,需要的朋友可以參考下2021-09-09