Servlet的Session机制流程如下
- 用户首次发请求
- 服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个Session用来储存该用户的信息,然后生成SessionId作为对应的Key
- 服务器会在响应中,用SessionId这个名字,把这个SessionId以cookie的方式发给客户(就是Set-cookie响应头)
- 由于已经设置了cookie,下次访问的时候,服务器会自动识别到这个SessionId然后找到你上次对应的Session
引入Shiro后,新的流程如下
- 用户首次发请求。
- 服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个Session用来储存该用户的信息,然后生成SessionId作为对应的Key,还会创建一个Subject对象(就是Shiro中用来代表当前用户的类),也用这个SessionId作为Key绑定。
- 服务器会在响应中,用SessionId这个名字,把这个SessionId以cookie的方式发给客户(就是Set-cookie响应头)。
- 第二次接受到请求的时候,Shiro会从请求头中找到SessionId,然后去寻找对应的Subject然后绑定到当前上下文,这时候Shiro就能知道来访的是谁了。
对于以上流程,都和浏览器中的cookie密切相关的,对于一个前后端分离的系统而言,一般是需要支持多端的,一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题
2、整合流程Shiro集成JWT需要禁用session,禁用后服务器将不会再维护用户的状态,达到无状态调用的目的
@Bean("defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(CustomRealm realm) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//开启全局缓存
realm.setCachingEnabled(true);
//开启认证缓存
realm.setAuthenticationCachingEnabled(true);
realm.setAuthenticationCacheName("authenticationCache");
//开启授权缓存
realm.setAuthorizationCachingEnabled(true);
realm.setAuthorizationCacheName("authorizationCache");
realm.setCacheManager(new EhCacheManager());
defaultSecurityManager.setRealm(realm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageevaluator defaultSessionStorageevaluator = new DefaultSessionStorageevaluator();
defaultSessionStorageevaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageevaluator(defaultSessionStorageevaluator);
defaultSecurityManager.setSubjectDAO(subjectDAO);
return defaultSecurityManager;
}
public class NoSessionWebSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 禁用session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
然后定义一个JwtToken,用于封装UserName和Token,并且要使其充当Shiro中的令牌,所以需要实现AuthenticationToken接口,重写里面的获取用户信息getPrincipal和获取凭证信息的getCredentials两个方法
public class JwtToken implements AuthenticationToken {
private final String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
定义Jwt相关的工具类
public class JwtUtil {
public static final String ACCOUNT = "userName";
public final static String CURRENT_TIME_MILLIS = "currentTimeMillis";
public static final long EXPIRE_TIME = 2 * 60 * 60 * 1000L;
public static final String SECRET_KEY = "shiroKey";
public static String sign(String userName, String currentTimeMillis) {
// 帐号加JWT私钥加密
String secret = userName + SECRET_KEY;
// 此处过期时间,单位:毫秒,在当前时间到后边的20分钟内都是有效的
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//采用HMAC256加密
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withClaim(ACCOUNT, userName)
.withClaim(CURRENT_TIME_MILLIS, currentTimeMillis)
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
}
public static boolean verify(String token) {
String secret = getClaim(token, ACCOUNT) + SECRET_KEY;
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
verifier.verify(token);
return true;
}
public static String getClaim(String token, String claim) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
然后再定义一个JwtFilter用于过滤所有请求,继承BasicHttpAuthenticationFilter,将其交给Shiro验证
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
protected static final String AUTHORIZATION_HEADER = "Access-Token";
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
JwtToken token = new JwtToken(((HttpServletRequest) request).getHeader(AUTHORIZATION_HEADER));
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String jsonWebToken = ((HttpServletRequest) request).getHeader(AUTHORIZATION_HEADER);
String username = "";
if (StringUtils.isBlank(jsonWebToken)) {
jsonWebToken = "";
} else {
// 解码 jwt
DecodedJWT decodeJwt = JWT.decode(jsonWebToken);
username = decodeJwt.getClaim("userName").asString();
System.out.println(username + "登录");
}
JwtToken token = new JwtToken(jsonWebToken);
try {
// 交给自定义realm进行jwt验证和对应角色,权限的查询
getSubject(request, response).login(token);
} catch (AuthenticationException e) {
request.setAttribute("msg", "认证失败");
// 转发给指定的 controller, 进行统一异常处理
request.getRequestDispatcher("/exception").forward(request, response);
return false;
}
return true;
}
}
在自定义Realm中验证jwt,role,permission
@Component
public class CustomRealm extends AuthorizingRealm {
@Resource
UserServiceImpl userServiceImpl;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户名, 用户唯一标识
String username = JwtUtil.getClaim(principals.toString(), "userName");
User user = userServiceImpl.findByUserName(username);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
if (user == null) {
return null;
}
authorizationInfo.addRole("admin");
authorizationInfo.addStringPermission("user:update:*");
authorizationInfo.addStringPermission("product:*:*");
return authorizationInfo;
}
@SneakyThrows
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String credentials = (String) token.getCredentials();
String userName;
try {
//jwt验证token
boolean verify = JwtUtil.verify(credentials);
if (!verify) {
throw new AuthenticationException("Token校验不正确");
}
userName = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
} catch (Exception e) {
throw new Exception("用户身份校验失败");
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
return new SimpleAuthenticationInfo(
//用户名
userName,
//凭证
credentials,
//realm name
this.getName());
}
}
定义一个登录接口
@RequestMapping("/login")
public String login(String userName, String password, HttpServletResponse response) {
try {
if (!"phz".equals(userName) || !"123".equals(password)) {
System.out.println("用户名错误");
return "redirect:/login.jsp";
}
//生成token
String token = JwtUtil.sign(userName, System.currentTimeMillis());
//写入header
response.setHeader("Access-Token", token);
response.setHeader("Access-Control-Expose-Headers", "Access-Token");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
return "redirect:/login.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
return "redirect:/login.jsp";
}
return "redirect:/index.jsp";
}
最后将自己定义的Filter添加到Shiro中
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加自定义过滤器
Map filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//配置系统受限资源和公共资源
Map map = new HashMap<>();
map.put("/register.jsp", ANON);
map.put("/login.jsp", ANON);
map.put("/user/login", ANON);
map.put("/user/register", ANON);
//使用自己的filter对所有请求拦截
map.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//不设置默认也是login.jsp
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
return shiroFilterFactoryBean;
}



