使用Maven搭建SpringMVC项目,并加入Spring Security的实现
参考: http://www.tuicool.com/articles/R7Rj6r3
官网: https://jwt.io/introduction/
JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
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
1.用户携带username和password请登录
2.服务器验证登录验证,如果验证成功,根据用户的信息和服务器的规则生成JWT Token
3.服务器将该token返回
4.用户得到token,存在localStorage、cookie或其它数据存储形式中。
5.以后用户请求服务器时,在请求的header中加入 Authorization: Bearer xxxx(token) 。此处注意token之前有一个7字符长度的“Bearer “,服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和业务逻辑反回响应结果。
io.jsonwebtoken jjwt 0.6.0
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) { Mapclaims = 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 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); }
@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(); } }
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); } }
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); } }
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只有一个map,缓存用户信息,实际工作中可以引入真实缓存工具来实现。
/** * 存入user token,可以引用缓存系统,存入到缓存。 */ @Component public class UserRepository { private static final MapuserMap = new HashMap (); 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); } }