今日主要在搭建一个新的项目,原先项目已采用Shiro JWT的方式实现登陆,为了丰富新项目权限控制的功能,采用Spring Security进行权限控制。希望2套系统通过JWT达到单点登陆的目的。由于项目规模过小,以快速开发为目的,所以不采用统一的auth服务器。但在进行构建时候,参考网上一些教程,发现网上常见的教程存在诸多问题。大多数文章都是copy同一篇文章,问题一直遗留。所以决定写一下这篇文章供大家参考。如果有错误的地方,希望大家能给我指正。
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。
JSON Web Tokens由点(.)分隔的三个部分组成,它们是:
{
"alg": "HS256",//签名或摘要算法
"typ": "JWT"//token类型
}
复制代码
{
"iss": "token-server",//签发者
"exp ": "Mon Nov 13 15:28:41 CST 2017",//过期时间
"sub ": "wangjie",//用户名
"aud": "web-server-1"//接收方,
"nbf": "Mon Nov 13 15:40:12 CST 2017",//这个时间之前token不可用
"jat": "Mon Nov 13 15:20:41 CST 2017",//签发时间
"jti": "0023",//令牌id标识
"claim": {“auth”:”ROLE_ADMIN”}//访问主张
}
复制代码
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 复制代码
每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中。标题的内容应如下所示:
Authorization: Bearer <token> 复制代码
在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查Authorization标头中的有效JWT ,如果存在,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。
public class JWTUtil {
问题代码
/**
* 校验token是否正确
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
修订后代码
token中已经包含Playload信息(username),此处进行校验不需要再传入username
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
复制代码
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* 将非法请求返回json
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setStatus(HttpServletResponse.SC_OK);
httpResponse.getWriter().write("{/"code/":401,/"msg/":/"未授权!/",/"ret/":false}");
return false;
}
}
复制代码
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
String username = jwtTokenUtil.getUsernameFromToken(authToken);
logger.info("checking authentication " + username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
//重点观察此处 从token中得到了用户名,再通过用户名从数据库中获取用户的详细信息
//接下来我们看一下jwtTokenUtil.validateToken()到底做了什么事
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
@Component
public class JwtTokenUtil implements Serializable {
/**
* 验证令牌
*/
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
String username = getUsernameFromToken(token);
//这里我看得一脸懵逼 不应该是校验token的合法性。
//这里居然用数据库中查到的username 和token的username比较。而数据中查询的username又是通过token的来的。
//所以这段代码有错误
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
复制代码