Spring Security + JWT 實現基於token的安全驗證
準備工作
使用Maven搭建SpringMVC項目,並加入Spring Security的實現
JWT簡介
參考: http://www.tuicool.com/articles/R7Rj6r3
官網: https://jwt.io/introduction/
JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標準定義的一種可以安全傳輸的 小巧 和 自包含 的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。
JWT的結構
JWT包含了使用 . 分隔的三部分:
1.Header 頭部,包含了兩部分:token類型和採用的加密算法。
2.Payload 負載,Token的第二部分是負載,它包含了claim, Claim是一些實體(通常指的用戶)的狀態和額外的元數據。
3.Signature 簽名,創建簽名需要使用編碼後的header和payload以及一個祕鑰,使用header中指定簽名算法進行簽名。
下面是一個jjwt生成的token
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJkZmRzYSIsImNyZWF0ZWQiOjE0OTQ5MjgzODQ1MzksInJvbGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FOT05ZTU9VUyJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0seyJhdXRob3JpdHkiOiJST0xFX0RCQSJ9XSwiaWQiOjAsImV4cCI6MTQ5NTUzMzE4NH0.RAWhCcFj7sfXI81zJ8fm0Rfb0IpwT7mNfuFPGzU6AblW2UdOgMtDExXlWZEr3pracdytsfw3os4dnJKM6ZW9mA
通過base64解碼上面token可以得到基本信息。
第一段爲Header信息,第二段爲Payload信息,最後一段其實是簽名,這個簽名必須知道祕鑰才能計算。這個也是JWT的安全保障。
注意事項,由於數據聲明(Claim)是公開的,千萬不要把密碼等敏感字段放進去。
{"alg":"HS512"}{"sub":"dfdsa","created":1494928384539,"roles":[{"authority":"ROLE_ANONYMOUS"},{"authority":"ROLE_ADMIN"},{"authority":"ROLE_USER"},{"authority":"ROLE_DBA"}],"id":0,"exp":1495533184}hBpX뱵ȳ\ɱ鴅洢쓮c_蓆͎க摓ಐąyVdJ禶췫l 資g$㺥of
JWT的工作流程
1.用戶攜帶username和password請登錄
2.服務器驗證登錄驗證,如果驗證成功,根據用戶的信息和服務器的規則生成JWT Token
3.服務器將該token返回
4.用戶得到token,存在localStorage、cookie或其它數據存儲形式中。
5.以後用戶請求服務器時,在請求的header中加入 Authorization: Bearer xxxx(token) 。此處注意token之前有一個7字符長度的“Bearer “,服務器端對此token進行檢驗,如果合法就解析其中內容,根據其擁有的權限和業務邏輯反迴響應結果。
實現JWT支持
添加Jar
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.6.0</version> </dependency>
創建JwtTokenUtils
private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_ID = "id"; private static final String CLAIM_KEY_CREATED = "created"; private static final String CLAIM_KEY_ROLES = "roles"; @Value("${jwt.token.secret}") private String secret; @Value("${jwt.token.expiration}") private int expiration; //過期時長,單位爲秒,可以通過配置寫入。 public String getUsernameFromToken(String token) { String username; try { username =getClaimsFromToken(token).getSubject(); } catch (Exception e) { username = null; } return username; } public Date getCreatedDateFromToken(String token) { Date created; try { final Claims claims = getClaimsFromToken(token); created = new Date((Long) claims.get(CLAIM_KEY_CREATED)); } catch (Exception e) { created = null; } return created; } public Date getExpirationDateFromToken(String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null; } return expiration; } private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } public String generateToken(User userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); claims.put(CLAIM_KEY_ID, userDetails.getId()); claims.put(CLAIM_KEY_ROLES, userDetails.getAuthorities()); return generateToken(claims); } public String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Boolean canTokenBeRefreshed(String token) { return !isTokenExpired(token); } public String refreshToken(String token) { String refreshedToken; try { final Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } public Boolean validateToken(String token, UserDetails userDetails) { User user = (User) userDetails; final String username = getUsernameFromToken(token); final Date created = getCreatedDateFromToken(token); return ( username.equals(user.getUsername()) && isTokenExpired(token)==false); }
修改WebSecurityConfig
@Configuration @EnableWebSecurity //添加annotation 支持,包括(prePostEnabled,securedEnabled...) @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http. // 由於使用的是JWT,我們這裏不需要csrf csrf().disable() // 基於token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() //所有用戶可以訪問"/resources"目錄下的資源以及訪問"/home"和favicon.ico .antMatchers("/resources/**", "/home","/**/favicon.ico","/auth/*").permitAll() //以"/admin"開始的URL,並需擁有 "ROLE_ADMIN" 角色權限,這裏用hasRole不需要寫"ROLE_"前綴; .antMatchers("/admin/**").hasRole("ADMIN") //以"/admin"開始的URL,並需擁有 "ROLE_ADMIN" 角色權限和 "ROLE_DBA" 角色,這裏不需要寫"ROLE_"前綴; .antMatchers("/dba/**").access("hasRole('ADMIN') and hasRole('DBA')") //前面沒有匹配上的請求,全部需要認證; .anyRequest().authenticated() .and() //指定登錄界面,並且設置爲所有人都能訪問; .formLogin().loginPage("/login").permitAll() //如果登錄失敗會跳轉到"/hello" .successForwardUrl("/hello") .successHandler(loginSuccessHandler()) //如果登錄失敗會跳轉到"/logout" //.failureForwardUrl("/logout") .and() .logout() .logoutUrl("/admin/logout") //指定登出的地址,默認是"/logout" .logoutSuccessUrl("/home") //登出後的跳轉地址login?logout //自定義LogoutSuccessHandler,在登出成功後調用,如果被定義則logoutSuccessUrl()就會被忽略 .logoutSuccessHandler(logoutSuccessHandler()) .invalidateHttpSession(true) //定義登出時是否invalidate HttpSession,默認爲true //.addLogoutHandler(logoutHandler) //添加自定義的LogoutHandler,默認會添加SecurityContextLogoutHandler .deleteCookies("usernameCookie","urlCookie") //在登出同時清除cookies ; // 禁用緩存 http.headers().cacheControl(); // 添加JWT filter http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } @Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder // 設置UserDetailsService .userDetailsService(this.userDetailsService) // 使用MD5進行密碼的加密 .passwordEncoder(passwordEncoder()); } private Md5PasswordEncoder passwordEncoder() { return new Md5PasswordEncoder(); } private AccessDeniedHandler accessDeniedHandler(){ AccessDeniedHandlerImpl handler = new AccessDeniedHandlerImpl(); handler.setErrorPage("/login"); return handler; } @Bean public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationTokenFilter(); } @Bean public LoginSuccessHandler loginSuccessHandler(){ LoginSuccessHandler handler = new LoginSuccessHandler(); return handler; } @Bean public LogoutSuccessHandler logoutSuccessHandler(){ return new LogoutSuccessHandler(); } }
創建JwtAuthenticationTokenFilter
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); @Resource private UserDetailsService userDetailsService; @Resource private JwtTokenUtils jwtTokenUtils; @Resource private UserRepository userRepository; private String tokenHeader = "Authorization"; private String tokenHead = "Bearer "; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { //先從url中取token String authToken = request.getParameter("token"); String authHeader = request.getHeader(this.tokenHeader); if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(tokenHead)) { //如果header中存在token,則覆蓋掉url中的token authToken = authHeader.substring(tokenHead.length()); // "Bearer "之後的內容 } if (StringUtils.isNotBlank(authToken)) { String username = jwtTokenUtils.getUsernameFromToken(authToken); logger.info("checking authentication {}", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { //從已有的user緩存中取了出user信息 User user = userRepository.findByUsername(username); //檢查token是否有效 if (jwtTokenUtils.validateToken(authToken, user)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //設置用戶登錄狀態 logger.info("authenticated user {}, setting security context",username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
創建LoginSuccessHandler
public class LoginSuccessHandler implements AuthenticationSuccessHandler { protected Logger logger = LoggerFactory.getLogger(LoginSuccessHandler.class); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Resource private UserDetailsService userDetailsService; @Resource private JwtTokenUtils jwtTokenUtils; @Resource private UserRepository userRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { final User userDetails = (User)userDetailsService.loadUserByUsername(authentication.getName()); final String token = jwtTokenUtils.generateToken(userDetails); userRepository.insert(userDetails); handle(request, response, authentication,token); clearAuthenticationAttributes(request); } protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication,String token) throws IOException { String targetUrl = determineTargetUrl(authentication); if (response.isCommitted()) { logger.debug( "Response has already been committed. Unable to redirect to " + targetUrl); return; } redirectStrategy.sendRedirect(request, response, targetUrl+"?token="+token); } /** * * 實現自定義的跳轉邏輯 * * @param authentication * @return */ protected String determineTargetUrl(Authentication authentication) { boolean isUser = false; boolean isAdmin = false; Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority grantedAuthority : authorities) { if (grantedAuthority.getAuthority().equals("ROLE_USER")) { isUser = true; break; } else if (grantedAuthority.getAuthority().equals("ROLE_ADMIN")) { isAdmin = true; break; } } if (isUser) { return "/websocket"; } else if (isAdmin) { return "/stomp"; } else { throw new IllegalStateException(); } } protected void clearAuthenticationAttributes(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return; } session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } }
創建LogoutSuccessHandler
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler { protected Logger logger = LoggerFactory.getLogger(LogoutSuccessHandler.class); @Resource private UserRepository userRepository; @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { logger.info("logout user {}",authentication.getName()); //登出後清除用戶緩存信息 userRepository.remove(authentication.getName()); } }
創建UserRepository
UserRepository只有一個map,緩存用戶信息,實際工作中可以引入真實緩存工具來實現。
/** * 存入user token,可以引用緩存系統,存入到緩存。 */ @Component public class UserRepository { private static final Map<String,User> userMap = new HashMap<String,User>(); public User findByUsername(final String username){ return userMap.get(username); } public User insert(User user){ userMap.put(user.getUsername(),user); return user; } public void remove(String username){ userMap.remove(username); } }