存在问题
分布式会话缓存是基于浏览器的Cookie机制,如果用户禁用Cookie,则无法实现功能。
解决方案
使用基于jwt的token生成方案:
- 用户登录之后,获得会话的SessionID,使用jwt根据SessionID颁发签名并设置过期时间(与Session过期时间相同)返回token
- 将token保存在客户端,并且每次发送请求时都在header上携带jwt的token
- shiroSessionManager继承DefaultWebSessionManager,重写 getSessionId方法。从header上检测是够携带token,如果携带,则解码token,使用jwttoken的jti作为sessionId
jwt概述
JWT(JSON WEB TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。
-
广义上:JWT是一个标准的名称;
-
狭义上:JWT指的就是用来传递的那个token字符串
JWT由三部分构成:header(头部)、payload(载荷)和signature(签名)。
-
Header
存储两个变量
- 秘钥(可以用来比对)
- 算法(也就是下面将Header和payload加密成Signature)
-
payload
存储很多东西,基础信息有如下几个
- 签发人,也就是这个"令牌"归属于哪个用户。一般是
userId - 创建时间,也就是这个令牌是什么时候创建的
- 失效时间,也就是这个令牌什么时候失效(session的失效时间)
- 唯一标识,一般可以使用算法生成一个唯一标识(jti==>sessionId)
- 签发人,也就是这个"令牌"归属于哪个用户。一般是
-
Signature
这个是上面两个经过Header中的算法加密生成的,用于比对信息,防止篡改Header和payload
然后将这三个部分的信息经过加密生成一个JwtToken的字符串,发送给客户端,客户端保存在本地。当客户端发起请求的时候携带这个到服务端(可以是在cookie,可以是在header),在服务端进行验证,我们需要解密对于的payload的内容
集成jwt
编写JwtProperties配置类
java
package com.itheima.shiro.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
/**
* @Description:jw配置文件
*/
@Data
@ConfigurationProperties(prefix = "itheima.framework.jwt")
public class JwtProperties implements Serializable {
/**
* @Description 签名密码
*/
private String base64EncodedSecretKey;
}
application.yml中jwt的相关配置
yaml
itheima:
resource:
systemcode: shiro-mgt
framework:
jwt:
base64-encoded-secret-key: qazwsx1234567890
编写JwtTokenManager类
java
package com.itheima.shiro.core.impl;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.itheima.shiro.config.JwtProperties;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.utils.EncodesUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
import javax.xml.crypto.Data;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Description:自定义jwtkoken管理者
*/
@Service("jwtTokenManager")
@EnableConfigurationProperties({JwtProperties.class})
public class JwtTokenManager {
@Autowired
JwtProperties jwtProperties;
/**
* @Description 签发令牌
* 1、头部型
* 密码签名
* 加密算法
* 2、payload
* 签发的时间
* 唯一标识
* 签发者
* 过期时间
* @param iss 签发者
* @param ttlMillis 过期时间
* @param claims jwt存储的一些非隐私信息
* @return
*/
public String issuedToken(String iss, long ttlMillis, String sessionId, Map<String,Object> claims){
if (EmptyUtil.isNullOrEmpty(claims)){
claims = new HashMap<>();
}
//获取当前时间
long nowMillis = System.currentTimeMillis();
//获取加密签名
String base64EncodedSecretKey = EncodesUtil.encodeBase64(jwtProperties.getBase64EncodedSecretKey().getBytes());
//构建令牌
JwtBuilder builder = Jwts.builder()
.setClaims(claims)//构建非隐私信息
.setId(sessionId)//构建唯一标识,此时使用shiro生成的唯一id
.setIssuedAt(new Date(nowMillis))//构建签发时间
.setSubject(iss)//签发者
.signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey);//指定算法和秘钥
if (ttlMillis>0){
long expMillis = nowMillis+ttlMillis;
Date expData = new Date(expMillis);
builder.setExpiration(expData);//指定过期时间
}
return builder.compact();
}
/**
* @Description 解析令牌
* @param jwtToken 令牌字符串
* @return
*/
public Claims decodeToken(String jwtToken){
//获取加密签名
String base64EncodedSecretKey = EncodesUtil.encodeBase64(jwtProperties.getBase64EncodedSecretKey().getBytes());
//带着密码去解析字符串
return Jwts.parser()
.setSigningKey(base64EncodedSecretKey)
.parseClaimsJws(jwtToken)
.getBody();
}
/**
* @Description 校验令牌:1、头部信息和荷载信息是否被篡改 2、校验令牌是否过期
* @param jwtToken 令牌字符串
* @return
*/
public boolean isVerifyToken(String jwtToken){
//带着签名构建校验对象
Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getBase64EncodedSecretKey().getBytes());
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
//校验:如果校验1、头部信息和荷载信息是否被篡改 2、校验令牌是否过期 不通过则会抛出异常
jwtVerifier.verify(jwtToken);
return true;
}
}
重写DefaultWebSessionManager
ShiroSessionManager主要是添加jwtToken的jti作为会话的唯一标识
java
package com.itheima.shiro.core.impl;
import com.itheima.shiro.utils.EmptyUtil;
import io.jsonwebtoken.Claims;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* @Description:自定义会话管理器
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
//从请求中获得sessionId的key
private static final String AUTHORIZATION = "jwtToken";
//自定义注入的资源类型名称
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Autowired
JwtTokenManager jwtTokenManager;
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//判断request请求中是否带有jwtToken的key
String jwtToken = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果没有走默认的cookie获得sessionId的方式
if (EmptyUtil.isNullOrEmpty(jwtToken)){
return super.getSessionId(request, response);
}else {
//如果有走jwtToken获得sessionI的的方式
Claims claims = jwtTokenManager.decodeToken(jwtToken);
String id = (String) claims.get("jti");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
重写默认过滤器
BaseResponse返回统一json的对象
java
package com.itheima.shiro.core.base;
import com.itheima.shiro.utils.ToString;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @Description 基础返回封装
*/
@Data
public class BaseResponse extends ToString {
private Integer code ;
private String msg ;
private String date;
private static final long serialVersionUID = -1;
public BaseResponse(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public BaseResponse(Integer code, String msg, String date) {
this.code = code;
this.msg = msg;
this.date = date;
}
}
编写JwtAuthcFilter,校验访问是否合法和拒绝访问时返回信息的设置
使用wtTokenManager.isVerifyToken(jwtToken)校验颁发jwtToken是否合法,同时在拒绝的时候返回对应的json数据格式
java
package com.itheima.shiro.core.filter;
import com.alibaba.fastjson.JSONObject;
import com.itheima.shiro.constant.ShiroConstant;
import com.itheima.shiro.core.base.BaseResponse;
import com.itheima.shiro.core.impl.JwtTokenManager;
import com.itheima.shiro.core.impl.ShiroSessionManager;
import com.itheima.shiro.utils.EmptyUtil;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* @Description:自定义登录验证过滤器
*/
public class JwtAuthcFilter extends FormAuthenticationFilter {
private JwtTokenManager jwtTokenManager;
public JwtAuthcFilter(JwtTokenManager jwtTokenManager) {
this.jwtTokenManager = jwtTokenManager;
}
/**
* @Description 是否允许访问
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断当前请求头中是否带有jwtToken的字符串
String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
//如果有:走jwt校验
if (!EmptyUtil.isNullOrEmpty(jwtToken)){
boolean verifyToken = jwtTokenManager.isVerifyToken(jwtToken);
if (verifyToken){
return super.isAccessAllowed(request, response, mappedValue);
}else {
return false;
}
}
//没有没有:走原始校验
return super.isAccessAllowed(request, response, mappedValue);
}
/**
* @Description 访问拒绝时调用
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//判断当前请求头中是否带有jwtToken的字符串
String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
//如果有:返回json的应答
if (!EmptyUtil.isNullOrEmpty(jwtToken)){
BaseResponse baseResponse = new BaseResponse(ShiroConstant.NO_LOGIN_CODE,ShiroConstant.NO_LOGIN_MESSAGE);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSONObject.toJSONString(baseResponse));
return false;
}
//如果没有:走原始方式
return super.onAccessDenied(request, response);
}
}
编写 JwtPermsFilter
校验方式没变,只是改写了当访问拒绝时返回信息的方式
java
package com.itheima.shiro.core.filter;
import com.alibaba.fastjson.JSONObject;
import com.itheima.shiro.constant.ShiroConstant;
import com.itheima.shiro.core.base.BaseResponse;
import com.itheima.shiro.utils.EmptyUtil;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* @Description:自定义jwt的资源校验
*/
public class JwtPermsFilter extends PermissionsAuthorizationFilter {
/**
* @Description 访问拒绝时调用
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
//判断当前请求头中是否带有jwtToken的字符串
String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
//如果有:返回json的应答
if (!EmptyUtil.isNullOrEmpty(jwtToken)){
BaseResponse baseResponse = new BaseResponse(ShiroConstant.NO_AUTH_CODE,ShiroConstant.NO_AUTH_MESSAGE);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSONObject.toJSONString(baseResponse));
return false;
}
//如果没有:走原始方式
return super.onAccessDenied(request, response);
}
}
编写 JwtRolesFilter
java
package com.itheima.shiro.core.filter;
import com.alibaba.fastjson.JSONObject;
import com.itheima.shiro.constant.ShiroConstant;
import com.itheima.shiro.core.base.BaseResponse;
import com.itheima.shiro.utils.EmptyUtil;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* @Description:自定义jwt角色校验
*/
public class JwtRolesFilter extends RolesAuthorizationFilter {
/**
* @Description 访问拒绝时调用
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
//判断当前请求头中是否带有jwtToken的字符串
String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
//如果有:返回json的应答
if (!EmptyUtil.isNullOrEmpty(jwtToken)){
BaseResponse baseResponse = new BaseResponse(ShiroConstant.NO_ROLE_CODE,ShiroConstant.NO_ROLE_MESSAGE);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSONObject.toJSONString(baseResponse));
return false;
}
//如果没有:走原始方式
return super.onAccessDenied(request, response);
}
}
在ShiroConfig中配置自定义的过滤器
java
@Autowired
JwtTokenManager jwtTokenManager;
/**
* @Description 自定义拦截器定义
*/
private Map<String, Filter> filters() {
Map<String, Filter> map = new HashMap<String, Filter>();
map.put("role-or", new RolesOrAuthorizationFilter());
map.put("kicked-out", new KickedOutAuthorizationFilter(redissonClient(), redisSessionDao(), shiroSessionManager()));
map.put("jwt-authc", new JwtAuthcFilter(jwtTokenManager));
map.put("jwt-perms", new JwtPermsFilter());
map.put("jwt-roles", new JwtRolesFilter());
return map;
}
authentication.properties配置
properties
/static/**=anon
#登录链接不拦截
/login/**=anon
#访问/resource/**需要有admin的角色
/resource/**=role-or[MangerRole,SuperAdmin]
#前后端分离校验角色
/role/** = kicked-out,jwt-roles[SuperAdmin]
#其他链接是需要登录的
/**=kicked-out,authc
业务代码编写
LoginAction改写
java
/**
* @Description jwt的json登录方式
* @param loginVo
* @return
*/
@RequestMapping("login-jwt")
@ResponseBody
public BaseResponse LoginForJwt(@RequestBody LoginVo loginVo){
return loginService.routeForJwt(loginVo);
}
LoginService
java
/**
* @Description jwt方式登录
@param loginVo 登录参数
* @return
*/
public BaseResponse routeForJwt(LoginVo loginVo) throws UnknownAccountException,IncorrectCredentialsException;
LoginServiceImpl
java
@Override
public BaseResponse routeForJwt(LoginVo loginVo) throws UnknownAccountException, IncorrectCredentialsException {
//实现登录
String jwtToken = null;
try {
SimpleToken simpleToken = new SimpleToken(null, loginVo.getLoginName(), loginVo.getPassWord());
Subject subject = SecurityUtils.getSubject();
subject.login(simpleToken);
//登录完成之后需要颁发令牌
String sessionId = ShiroUserUtil.getShiroSessionId();
ShiroUser shiroUser = ShiroUserUtil.getShiroUser();
Map<String,Object> claims = new HashMap<>();
claims.put("shiroUser", JSONObject.toJSONString(shiroUser));
jwtToken = jwtTokenManager.issuedToken("system", subject.getSession().getTimeout(), sessionId, claims);
//缓存加载
this.loadAuthorityToCache(simpleToken);
}catch (Exception e){
BaseResponse baseResponse = new BaseResponse(ShiroConstant.LOGIN_FAILURE_CODE, ShiroConstant.LOGIN_FAILURE_MESSAGE);
}
BaseResponse baseResponse = new BaseResponse(ShiroConstant.LOGIN_SUCCESS_CODE, ShiroConstant.LOGIN_SUCCESS_MESSAGE,jwtToken);
return baseResponse;
}
LoginVo类(说明)
java
package com.itheima.shiro.vo;
import com.itheima.shiro.utils.ToString;
import lombok.Getter;
import lombok.Setter;
/**
*
* @ClassName: LoginVo
*
*/
@Getter
@Setter
public class LoginVo extends ToString {
/** serialVersionUID */
private static final long serialVersionUID = 8393603366418306749L;
/**登录名**/
private String loginName;
/**登录密码**/
private String passWord;
/**图片验证码**/
private String radompicture;
/**图片验证码key**/
private String key;
/**终端信息*/
private String blackBox;
/**终端类型(ios、android、h5)*/
private String terminalType;
/**登陆方式**/
private String loginType;
/**微信openId**/
private String openId;
/**系统**/
private String systemCode;
/**短信验证码**/
private String checkCode;
/**应用下载渠道code**/
private String channelNo;
/**推荐码**/
private String recommendId;
/**产品类型**/
private String productType;
}