您的位置:首页 > 编程语言 > Java开发

Spring Security + JWT 实现基于Token的安全验证

2017-05-16 18:44 1051 查看


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
.userD
ce0b
etailsService(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);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: