Spring Security JWT
Spring Security国内貌似还是比较冷门,很多时候想用SS(spring security)搞点事情都没什么资源,幸亏谷歌大法好。老实说,国外的包容性是真的好,什么冷门技术都有一席之地...
什么是JWT
首先JWT(json web token)只是一个轻巧的规范,并不是什么框架什么的。
一般讲JWT都会拿session比较,session的主要缺点是服务端需要保存在内存中,当会话多起来时是个问题,另外就是session不利于多应用通信(当然用spring session弄分布式会话也是可以的,但是存储消耗依然是个问题,存储就肯定需要查找,一说查找就肯定有比较大的消耗)。
于是有了token这种说法,客户端持有服务端颁发的token(就是一串字符串,但是会保存当前用户的信息,所以不应该放一些敏感信息在token里面,例如密码这种,因为token只有签名是加密的),服务端无须保存这个token。每次客户端发请求都把token发过来(一般放在请求头中),服务端根据一定的算法(公开密钥加密)来认证这个token是不是自己颁发的,如果是,则可以认为这个token的信息是可信的。
不过我依然有个问题,如果用户修改了JWT前两part的内容,例如他的权限,这样token就很危险了,难道又要查一下数据库或者缓存?希望大神解答一下
关于token的详细介绍,简书这篇文章说得很好。另外也可以看一下官方的一个介绍,有一个debug小工具玩一下,可以加深理解。
利用JWT已有实现实现JWT认证
在JWT的官网那里可以看到JWT规范的各种语言各种版本的实现。用哪个都没什么所谓,这里直接帖maven依赖。
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
思路:首先登录认证那块应该不需要改变,表单登录,json登录(可以看下我的上篇SS文章ヾノ≧∀≦)o )还是什么BASIC登录也好,我们需要在登录成功后给客户端一个jwt,也就是重写一个AuthenticationSuccessHandler
,另外就是客户端发一个jwt的时候需要认证,这里就需要写一个新的Filter
。另外就是一个封装JWT操作的类。一共三个东西,AuthenticationSuccessHandler
,Filter
,TokenUtils
。
虽然思路是有,不过借鉴下别人的做法会更好,这里我主要借鉴了SOF的一个问答。
TokenUtils
首先封装一下jwt的操作,我们无非就是要三种操作,生成一个jwt,验证客户端的jwt,解析jwt然后得到里面的内容。这里用到了一点lambda语法,不过即使不太懂大概也能看懂什么意思。这里我定义的token就存放用户名,权限,颁发时间和截至有效期。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.stream.Collectors;
/**
* Created by chenhuanming on 2017-08-09.
*
* @author chenhuanming
*/
@Component
public class TokenUtils {
//Secret密钥
private final String SECRET = "auth_chm";
//token有效期(分钟)
private final long VALIDATE_MINUTE = 30;
//加密算法
private final Algorithm algorithm;
public TokenUtils() throws UnsupportedEncodingException {
algorithm = Algorithm.HMAC256(SECRET);
}
/**
* 根据用户信息生成token
* @param authentication
* @return
*/
public String generateToken(Authentication authentication) {
String authorities = authentication.getAuthorities()
.stream()
.map(authority -> authority.getAuthority())
.collect(Collectors.joining(","));
Date now = Date.from(Instant.now());
Date expiration = Date.from(ZonedDateTime.now().plusMinutes(VALIDATE_MINUTE).toInstant());
//create jwt
String jwt = JWT.create()
.withClaim("authorities", authorities)
.withSubject(authentication.getName())
.withIssuedAt(now)
.withExpiresAt(expiration)
.sign(algorithm);
return jwt;
}
/**
* 认证token有效性
* @param token
* @return
*/
public boolean validateToken(String token) {
if(token==null)
return false;
try {
JWT.require(algorithm).build().verify(token);
return true;
} catch (JWTVerificationException e) {
return false;
}
}
/**
* 从token中解析中用户信息
* @param token
* @return
*/
public Authentication getAuthentication(String token) {
DecodedJWT decodedJWT = JWT.decode(token);
String authorityString = decodedJWT.getClaim("authorities").asString();
Collection<? extends GrantedAuthority> authorities = Collections.emptyList();
if(!StringUtils.isEmpty(authorityString)){
authorities = Arrays.asList(authorityString.split(","))
.stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
}
User principal = new User(decodedJWT.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
}
SuccessHandler
这里继承SavedRequestAwareAuthenticationSuccessHandler
,需要用jackson来写入json
@Component
public class SuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final TokenUtils tokenUtils;
private final ObjectMapper objectMapper;
private final JsonNodeFactory jsonNodeFactory;
public SuccessHandler(TokenUtils tokenUtils) {
this.tokenUtils = tokenUtils;
this.objectMapper = new ObjectMapper();
this.jsonNodeFactory = JsonNodeFactory.instance;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try (Writer writer = response.getWriter()){
JsonNode jsonNode = jsonNodeFactory.objectNode()
.put("token",tokenUtils.generateToken(authentication));
objectMapper.writeValue(writer,jsonNode);
}catch (Exception e){
e.printStackTrace();
}
}
}
TokenAuthorizationFilter
主角上场了,认证token的filter,这个filter就从请求头中拿到token字符串,然后用TokenUtils
检验token的合法性,如果合法就解析出相应的信息,然后组装成Authentication
,最后放到SecurityContext
中,就完事了。
@Component
public class TokenAuthorizationFilter extends GenericFilterBean {
private final String HEADER_NAME = "Authorization";
private final TokenUtils tokenUtils;
public TokenAuthorizationFilter(TokenUtils tokenUtils) {
this.tokenUtils = tokenUtils;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String token = resolveToken((HttpServletRequest) request);
if(tokenUtils.validateToken(token)){
Authentication authentication = tokenUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
cleanAuthentication();
}
/**
* 从请求头解析出token
* @param request
* @return token
*/
private String resolveToken(HttpServletRequest request){
String token = request.getHeader(HEADER_NAME);
if(token==null||!token.startsWith("Bearer "))
return null;
else
return token.substring(7);
}
private void cleanAuthentication(){
SecurityContextHolder.getContext().setAuthentication(null);
}
}
配置到SS
因为系统中已经不需要session了,所以告诉SS不生成session了,然后就是把SuccessHandler
和Filter
放进去。
http.antMatcher("/**")
.authorizeRequests().antMatchers("/me").authenticated()
.anyRequest().permitAll()
.and()
.formLogin().loginProcessingUrl("/login")
.successHandler(successHandler)
.and()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.addFilterBefore(tokenAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
效果图
用postman来测试。
客户端登录
客户端用拿到的token去请求一个受保护的API
最后附上github地址