1.自定义filter
拦截所有携带token的请求, 调用自定义realm,判断token是否正确,不正确realm抛出异常,在filter中被捕获,重定向至token不正确界面
重写了三个方法:
1》isAccessAllowed:如果带有 token,则对 token 进行检查,否则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true(放行)
2》isLoginAttempt:被isAccessAllowed调用,检测 header 里面是否包含 Token 字段。
3》executeLogin:实际的登录方法,当有token时,被isaccessallwed调用,执行subject.login方法,
即调用自定义realm中的doGetAuthenticationInfo方法,如果登录成功,isaccessallwed就返回true,如果登录失败,realm中抛出异常,抛到subject.login,被try catch捕获,重定向到无权限界面(因为filer父类BasicHttpAuthenticationFilter没有抛出异常,所以这里不能抛出,也就不能给springboot的异常处理器处理)
4》prehandle 处理跨域
package com.hxut.rj1192.dormitory2.filter;
import com.hxut.rj1192.dormitory2.pojo.JWTToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
// 所有的请求都会到达这个过滤器处理。
// 我们需要重写几个方法:
//
// isAccessAllowed:是否允许访问。如果带有 token,则对 token 进行检查,否则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
// isLoginAttempt:检测 header 里面是否包含 Token 字段。
// executeLogin:实际的登录方法
// preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
public class JWTFilter extends BasicHttpAuthenticationFilter {
//是否允许访问,如果带有 token,则对 token 进行检查 (true即允许通过,false不通过),否则直接通过
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
System.out.println("JWTFilter的isAccessAllowed方法 登录验证 调用isLoginAttempt方法判断是否有jwt 没有就放行有就验证jwt返回true 否则返回false");
//判断请求的请求头是否带上 token
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
// 因为父类没有抛出异常,所以子类也不能抛出异常
executeLogin(request, response);
return true;
} catch (AuthenticationException e) {
//token 错误
responseError(response, e.getMessage());
}
}
//如果请求头不存在 Token,则可能是执行登录操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Authorization");
System.out.println("JWTFilter的isLoginAttempt方法 判断请求头中有没有jwt" + (token == null));
return token != null;
}
/*
* 实际的登录方法,这里我们重写了这个方法
* 当登录失败时,realm 返回false login 会抛出异常,被上面调用executeLogin isAccessAllowed 方法捕获
* */
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
System.out.println("JWTFilter的executeLogin方法 验证token");
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Authorization");
System.out.println("请求头中的token " + token);
JWTToken jwt = new JWTToken(token);
//交给自定义的realm对象去登录,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwt);
return true;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-control-Allow-Origin", req.getHeader("Origin"));
res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
res.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 将非法请求跳转到 /unauthorized/**
*/
private void responseError(ServletResponse response, String message) {
System.out.println("跳转到错误页面");
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
try {
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
}
}
}
2.自定义AuthenticationToken 类
调用subject.logn方法 ,需要一个 AuthenticationToken 类,来存储用户信息,不设置就是默认的,有用户名密码等,但是我要验证的是jwt,不是用户名和密码,所以要写个新的AuthenticationToken类,来作为subject.login方法的参数,传递给realm中doGetAuthenticationInfo
package com.hxut.rj1192.dormitory2.pojo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
/*
JwtToken代替原生的UsernamePasswordToken ,所以要继承AuthenticationToken类
*/
public class JWTToken implements AuthenticationToken {
private String jwt;
public JWTToken(String jwt) {
this.jwt = jwt;
}
//获取用户名 ,这里直接返回token
@Override
public Object getPrincipal() {
return jwt;
}
//返回密码,这里直接返回token
@Override
public Object getCredentials() {
return jwt;
}
}
3.自定义realm 和不分离没有区别
package com.hxut.rj1192.dormitory2.realm;
import com.hxut.rj1192.dormitory2.mapper.UserMapper;
import com.hxut.rj1192.dormitory2.pojo.JWTToken;
import com.hxut.rj1192.dormitory2.pojo.User;
import com.hxut.rj1192.dormitory2.service.impl.CheckloginService;
import com.hxut.rj1192.dormitory2.util.JWTUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class AccountRealm extends AuthorizingRealm {
//根据token判断此Authenticator是否使用该realm
//必须重写不然shiro会报错
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如@RequiresRoles,@RequiresPermissions之类的
*/
@Autowired
UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("自定义realm的doGetAuthorizationInfo执行了~~~~~查询用户权限");
String token = principals.toString();
String username = JWTUtil.getUsername(token);
System.out.println("自定义realm的doGetAuthorizationInfo执行了~~~~~查询用户权限,查到的用户名" + username);
System.out.println("自定义realm的doGetAuthorizationInfo执行了~~~~~查询用户权限,查到的角色名" + userMapper.queryrole(username));
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//查询数据库获取用户的角色
info.addRole(userMapper.queryrole(username));
//查询数据库获取用户的权限
Set<String> querypermission = userMapper.querypermission(username);
for (String temp : querypermission) {
info.addStringPermission(temp);
}
return info;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可,在需要用户认证和鉴权的时候才会调用
*/
@Autowired
CheckloginService checkloginService;
// 获取token中的用户名,去数据库查询相应信息,如果为空,代表没有该用户
// 验证token是否正确
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getCredentials();
//decode时候出错,可能是token的长度和规定好的不一样了
String username = JWTUtil.getUsername(jwt);
System.out.println("accountrealm的 doGetAuthenticationInfo方法(验证token)中获取到的用户名" + username);
JWTUtil.verify(jwt);//如果错误,会抛一个 过期错误,或者一个token过期错误
User user = userMapper.querybyname(username);
if (user == null) {
throw new AuthenticationException("该用户不存在");
}
// 前后端不分离,返回的是用户名,密码 ,realm名
return new SimpleAuthenticationInfo(jwt, jwt, "AccountRealm");
}
}
4.shiroconfig
1》设置自定义的realm
2》设置关闭session
3.》开启shiro注解
4》设置拦截,拦截除了unauthorized/的所有路径,交给自定义filter去判断是否放行(/unauthorized/是没有权限时重定向的路径,如果没有放行,则带有token,被拦截,验证realm,抛出异常,跳转至/unauthorized重定向界面,被拦截,验证realm.....无限循环了)
package com.hxut.rj1192.dormitory2.config;
import com.hxut.rj1192.dormitory2.filter.JWTFilter;
import com.hxut.rj1192.dormitory2.realm.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义 realm
securityManager.setRealm(accountRealm);
//关闭session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 先走 filter ,filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new LinkedHashMap<>();
//设置自己的过滤器并且取名为jwt
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
Map<String, String> filterRuleMap = new HashMap<>();
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 除了无权限时跳转的路径
filterRuleMap.put("/unauthorized/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
/**
* 添加注解支持,如果不加的话很有可能注解失效
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 添加注解支持,如果不加的话很有可能注解失效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
5.jwtutil
package com.hxut.rj1192.dormitory2.util;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.hxut.rj1192.dormitory2.pojo.ReturnMap;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.shiro.authc.AuthenticationException;
import org.springframework.context.annotation.Configuration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class JWTUtil {
//设置过期时间 一分钟过期
private static final long EXPIRE_DATE = 60 * 1000 * 10;
// token 是给前端的一段有我自己标识(TOKEN_SECRET 标识这个toekn确实是服务器生成的 )的密码,
// 因为前后端分离的项目没有session cookie,就需要用token来标识用户,替代原来的session 和cookie
// 如果其他人想要伪造token,则必须要知道我的标识 即TOKEN_SECRET,所以别人不能伪造
// 如果更安全一点,可以使用md5对普通生成的toekn二次加密加盐,即使黑客得到了toekn,
// 它也不能破解出 toekn第二部分携带的信息(但是可以伪造用户去请求服务器(toekn就是密码,密码泄露了,肯定能登录的),所以toekn要设置有效期)
private static final String TOKEN_SECRET = "ZYKZYKZYKZYK";
public static String createToken(String username, String password) {
String token = "";
try {
//过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_DATE);
//秘钥及加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//设置头部信息
Map<String, Object> header = new HashMap<>();
header.put("typ", "JWT");
header.put("alg", "HS256");
//携带username,password信息,生成签名
token = JWT.create()
.withHeader(header)
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return token;
}
//无需解密也可以获取token的信息
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static boolean verify(String token) {
/**
* @desc 验证token,通过返回true
* @params [token]需要校验的串
**/
try {
DecodedJWT jwt2 = JWT.decode(token);
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (TokenExpiredException e) {
throw new AuthenticationException(JSON.toJSONString( ReturnMap.fail("token过期,登录失败","-11")));
}
catch(SignatureVerificationException e){
throw new AuthenticationException(JSON.toJSONString( ReturnMap.fail("token可能被篡改,或token错误,登录失败","-11")));
}
}
}
有点问题:每次带token的请求都会请求一次mysql,最好先查redis,没有再查mysql