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)
.sessionFixation().none()
//所有用户可以访问"/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);
}
}