1 簡介
Spring Security作為成熟且強大的安全框架,得到許多大廠的青睞。而作為前后端分離的SSO方案,JWT也在許多項目中應用。本文將介紹如何通過Spring Security實現JWT認證。
用戶與服務器交互大概如下:
- 客戶端獲取JWT,一般通過POST方法把用戶名/密碼傳給server;
- 服務端接收到客戶端的請求后,會檢驗用戶名/密碼是否正確,如果正確則生成JWT并返回;不正確則返回錯誤;
- 客戶端拿到JWT后,在有效期內都可以通過JWT來訪問資源了,一般把JWT放在請求頭;一次獲取,多次使用;
- 服務端校驗JWT是否合法,合法則允許客戶端正常訪問,不合法則返回401。
2 項目整合
我們把要整合的Spring Security和JWT加入到項目的依賴中去:
- <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工具類起碼要具有以下功能:
- 根據用戶信息生成JWT;
- 校驗JWT是否合法,如是否被篡改、是否過期等;
- 從JWT中解析用戶信息,如用戶名、權限等;
具體代碼如下:
- @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");
- }
- }
- }
工具類還實現了另一個功能:從HTTP請求頭中獲取JWT。
2.1.2 Token處理的Filter
Filter是Security處理的關鍵,基本上都是通過Filter來攔截請求的。首先從請求頭取出JWT,然后校驗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);
- }
- }
對于異常處理,使用@ControllerAdvice是不行的,應該這個是Filter,在這里拋的異常還沒有到DispatcherServlet,無法處理。所以Filter要自己做異常處理:
- catch (InvalidJwtAuthenticationException e) {
- response.setStatus(HttpStatus.UNAUTHORIZED.value());
- response.getWriter().write("Invalid token");
- response.getWriter().flush();
- return;
- }
最后的return不能省略,因為已經要把輸出的內容給Response了,沒有必要再往后傳遞,否則報錯
- java.lang.IllegalStateException: getWriter() has already been called
2.1.3 JWT屬性
JWT需要配置一個密鑰來加密,同時還要配置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的整個框架還是比較復雜的,簡化后大概如下圖所示:
它是通過一連串的Filter來進行安全管理。細節這里先不展開講。
2.2.1 WebSecurityConfigurerAdapter配置
這個配置也可以理解為是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配置了哪些請求需要什么權限才可以訪問。
- /auth/login用于登陸獲取JWT,所以都能訪問;
- /admin只有ADMIN用戶才可以訪問;
- /user只有USER用戶才可以訪問。
而之前實現的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的世界里,都是通過實現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"));
- }
- }
對于UserRepository
,可以從數據庫中讀取,或者其它用戶管理中心。為了方便,我使用Map放了兩個用戶:
- @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 測試
完成代碼編寫后,我們來測試一下:
(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,密碼錯誤則失敗,密碼正確則成功
- $ 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 總結
代碼請查看:https://github.com/LarryDpk/pkslow-samples
以上就是Springboot集成Spring Security實現JWT認證的步驟詳解的詳細內容,更多關于Springboot集成Spring Security的資料請關注服務器之家其它相關文章!