在上节课中我们使用的Shiro进行用户认证,内部通过是Session识别Subject,服务器识别依赖JSESSIONID的Cookie。但是在前后端分离的项目中,前端项目会单独运行挂载另外一个服务器中和后端项目的服务器不同。前端向后端发送请求时是跨域的无法携带JSESSIONID的Cokie的,就会导致每一个请求都是一个新的Session。问题的根源在传统的会话跟踪技术(Session+Cookie)存在弊端:
1.跨域问题
2.集群问题
那么必须找一个新的技术来实现会话跟踪,我们接触过一个东西token,令牌机制是一种很好的解决方案。但是在前面的应用中token存在数据库,需要频繁的访问数据库,这是极其影响服务器性能的。那么在传统的token之上再次升级就是Jwt(JSONS WEB TOKEN)。
要使用一种新的技术来完成会话跟踪,必须满足以下要求:
1.不能接触Cookie,要能够跨域传输
2.能够识别用户身份
3.足够安全
JWT是一种规范,是对token提出的一种标准。详细的描述了一个token应该具备哪些数据。每一段数据的具体作用如何。
JWT是一段字符串,这段字符串由三个部分组成,每一部分之间使用.分隔。例如:xxxxxxx.yyyyyyy.zzzzzz。
这三部分分别是:
Header(头信息):
头信息是一段JSON数据,描述jwt的加密方式和类型,这一段内容基本不变。
{
“alg”: “HS256”,//加密方式
“typ”: “JWT”//类型
}
将这样一段JSON进行Base64URL加密处理之后得到的就是JWT的第一段内容。
Payload(荷载)
荷载同样是一段JSON数据,描述JWT信息本体。规范中指出荷载可选的7个预定义属性为:
iss (issuer):签发人
sub (subject):主体,存储用户ID
iat (Issued At):签发时间
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
jti (JWT ID):编号
aud (audience):受众
例如:
{
“iss”: “http://localhost:8000/auth/login”,
“sub”: “1”,
“iat”: 1451888119,
“exp”: 1454516119,
“nbf”: 1451888119,
“jti”: “37c107e4609ddbcc9c096ea5ee76c667”,
“aud”: “dev”
}
将这样一段JSON数据进行Base64URL加密处理之后得到的就是JWT的第二段内容
signature(签名):
签名是JWT安全性的最大保障。因为Base64是可逆的,如果客户端将荷载使用Base64解密,修改sub,然后在加密覆盖原本的荷载,就可以伪造JWT。为了防止客户端伪造JWT,我们将Header的内容+Payload的内容进行一种安全性更高的加密(HS256),加密之后的内容就是签名。HS256是一种带秘钥的摘要加密,摘要加密的特点是不可逆。
我们在后端解析JWT识别用户身份,用户身份在荷载中,这个通过Base64可以拿到。但是在解析JWT之前我们会先校验JWT的合法性。
签名对jwt安全性保障:
使用第三方的JWT生成器:
引入依赖:
<!-- jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
JWT工具类:
//JWT工具类 public class JWTUtils { //定义加密秘钥 private final static String KEY="wuyanzudemiyao"; //定义JWT的有效时间 private final static long TIME=3*24*60*60*1000; /* 生成JWT的方法 */ public static String generatorJWT(String id){ JwtBuilder builder = Jwts.builder() .setSubject(id)//设置用户ID .setIssuedAt(new Date())//设置签发时间 .setExpiration(new Date(new Date().getTime()+TIME))//设置过期时间 .signWith(SignatureAlgorithm.HS256, KEY);//设置签名方式和秘钥 String token = builder.compact(); return token; } /* 校验JWT */ public static void validateJWT(String token) throws Exception{ Jwts.parser().setSigningKey(KEY).parseClaimsJws(token); } /* 解析token并获取subject */ public static String getId(String token) throws Exception{ Claims claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody(); return claims.getSubject(); } }
Jwt用于用户身份认证业务流程:
Jwt签发:
jwt校验:
项目和依赖和前面的Shiro项目一致
• 实现登陆并签发token
在前面的认证中,登陆放到了领域中,在Shiro+Jwt做认证的项目中登陆还是放到原本的控制器中。登陆成功以后生成一个token,将token放到JSONResult一起响应给客户端。原本的领域我们只用来进行token的校验。
@RestController @RequestMapping("user") @CrossOrigin public class UserController { @GetMapping("login") public JSONResult login(String username, String password) throws Exception{ if("admin".equals(username)&&"123456".equals(password)){ //使用工具生成token String token = JWTUtils.generatorJWT("1"); return new JSONResult("1001","success",token,null); } return new JSONResult("1001","fail",null,null); } }
<script> $("#btn").click(function(){ $.ajax({ url:"http://localhost/user/login", type:"get", data:$("#login-form").serialize(), success:function(data){ alert(data.message); //将token存储到localStorage localStorage.setItem("token",data.object); } }); }); </script>
• 前端在发送请求时需要在请求头中携带token
$.ajax({ url:"http://localhost/user", type:"get", headers:{"token":localStorage.getItem("token")}, success:function(data){ alert(data.object); } });
• 在控制器中可以从请求头中取出token
从请求头中取出token,使用JWTUtils取出其中的subject数据,参与业务
@GetMapping public JSONResult selectUser(@RequestHeader("token") String token) throws Exception{ String id=JWTUtils.getId(token); return new JSONResult("1001","success","项神YYDS",null); }
• 使用Shiro完成JWT的认证
自定义一个过滤器,在该过滤器中完成对于jwt的校验
public class JWTFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { //返回值true表示请求向下继续执行 //判断请求头中是否包含token HttpServletRequest req= (HttpServletRequest) request; if(req.getHeader("token")!=null){ //调用认证方法,认证结果就代表本次是否放行 try { return executeLogin(request,response); } catch (Exception e) { e.printStackTrace(); } } //返回值false shiro会抛出401异常 return false; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req= (HttpServletRequest) request; //调用领域类中的方法执行认证 Subject subject = SecurityUtils.getSubject(); JWTToken jwtToken = new JWTToken(req.getHeader("token")); //让shiro通过领域类完成认证 subject.login(jwtToken);//执行认证 不是登陆 return true; } @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = WebUtils.toHttp(request); HttpServletResponse httpServletResponse = WebUtils.toHttp(response); //处理跨域 httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); //如果请求方式是options,代表着是预检请求,因此,直接放行 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
要去执行认证需要传入Shiro中的Token类对象,需要自定义一个Token,用户名和密码都是jwt的token
public class JWTToken implements AuthenticationToken { public JWTToken(String token){ this.token=token; } private String token; public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
在领域类中处理认证逻辑
public class JWTRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken;//判定token类型,如果是JWTToken则允许执行认证 } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token= (String) authenticationToken.getPrincipal(); //进行token校验 //通过校验说明token有效 返回认证信息 try { JWTUtils.validateJWT(token); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(token, token,getName()); return info; } catch (Exception e) { //没有通过校验返回null e.printStackTrace(); return null; } } }
• 将自定义过滤器配置到Shiro中
在之前的项目中,我们使用的anon、user、logout都是Shiro自带的过滤器,我们要通过过滤器实现JWT校验需要将自己的过滤器添加进去。
@Configuration public class ShiroConfig { @Bean public JWTRealm initUserRealm(){ return new JWTRealm(); } @Bean public SecurityManager initSecurityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(initUserRealm()); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilter() throws UnsupportedEncodingException { //实例化Shiro过滤器工厂 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //在工厂中注入安全管理器 shiroFilterFactoryBean.setSecurityManager(initSecurityManager()); //将我们自己的filter添加到Shiro Map<String, Filter> filters = shiroFilterFactoryBean.getFilters(); filters.put("jwt",new JWTFilter()); //创建一个有序键值对用于存储黑白名单 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //anon表示无须登陆就能访问的资源地址 filterChainDefinitionMap.put("/user/login", "anon"); //其余所有请求地址均需要通过jwt校验 filterChainDefinitionMap.put("/**", "jwt"); //将黑白名单配置到shiro过滤器 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } }
Shiro+JWT的授权过程和单独使用Shiro的授权过程是一样的。唯一的区别是在查询用户权限时,需要的用户ID不能从Session中获取的,而是从Subject取出存进去的token,将token进行解析得到其中的id。
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //取出token String token= (String) principalCollection.getPrimaryPrincipal(); //使用JWTUtils解析并获取subject String id = null; SimpleAuthorizationInfo info=null; System.out.println("授权"); try { id = JWTUtils.getId(token); System.out.println(id); //模拟查询数据库权限 info= new SimpleAuthorizationInfo(); info.addStringPermission("角色管理"); info.addStringPermission("新增角色"); info.addStringPermission("用户管理"); } catch (Exception e) { e.printStackTrace(); } return info; }
无论你在学习上有任何问题,重庆蜗牛学院欢迎你前来咨询,联系QQ:296799112