目录
shiro简介
shiro是java非常有名的认证与授权的框架,使用JavaEE中JAAS功能。设计简单,SpringSecurity必须使用spring项目中。
1.认证
核验用户身份,认证登录,登录后Shiro要记录用户成功登录的凭证
2.授权
比再认证更加精细度的划分用户的行为。比如学生和班主任,可以可以改成绩,一个不可以改成绩。
3.shiro靠什么做认证与授权
主要利用HttpSession或者Redis存储用户的登录平成,以及角色或者身份信息。然后利用过滤器(Filter),对每个Http请求过滤,检查请求对应的HttpSession或Redis中的认证与授权。如果用户没有登录,或权限不够,那么Shiro就会向和护短返回错误信息。
JWT简介
主要做单点登录
未使用JWT,三个节点三个tomcat做负载均衡,A节点登录,B节点无法获取用户信息
使用JWT,对登录凭证进行加密,并保存到客户端,并叫做令牌,每次发送请求的时候,都把令牌上传到服务器,
JWT兼容更多的客户端
传统的HttpSession依靠浏览器的Cookie存放SessionID,所以要求客户端浏览器。
但现在javaweb系统,客户端可以是浏览器,APP,小程序,以及物联网设备,为了让javaweb都访问到项目,引用JWT技术。
jwt的token是纯字符串,对保存没要求,只要客户端发起请求的时候带上token即可。可以使用SQLite存储Token数据
创建JWTUtil工具类
导入依赖库
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义密钥的和过期时间
在application.yml中配置
emos:
jwt:
#密钥
secret: abc123456
#令牌过期时间(天)
expire: 5
#令牌缓存时间(天数)
cache-expire: 10
生成令牌并验证令牌的有效性
令牌的生成需要密钥、过期时间、用户ID
配置类
@Component
@Slf4j
public class JwtUtil {
@Value("${emos.jwt.secret}")
private String secret;
@Value("${emos.jwt.expire}")
private int expire;
public String createToken(int userId){
// 当前日期偏移5天
Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, 5);
// 加密算法,调用静态工厂方法进行调用
Algorithm algorithm=Algorithm.HMAC256(secret);
// 创建内部类
JWTCreator.Builder builder= JWT.create();
String token = builder.withClaim("userId", userId).withExpiresAt(date).sign(algorithm);
return token;
}
// 获取user的用户ID
public int getUserId(String token){
// 创建解码的对象
DecodedJWT jwt = JWT.decode(token);
int userId = jwt.getClaim("userId").asInt();
return userId;
}
// 验证令牌字符串的有效性
public void verifierToken(String token){
Algorithm algorithm=Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
// 验证方法回抛出runtime的异常
verifier.verify(token);
}
}
令牌封装成对象
主要4个类
AuthenticationToken类
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
AuthorizingRealm类
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private JwtUtil jwtUtil;
// 传入封装好的令牌对象
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 创建认证对象
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
//TODO 查询用户的全下班列表
//TODO 把权限列表添加到info对象中
return info;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//创建授权对象
// todo 从令牌中获取userID,然后检测该账户是否被冻结
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
//todo 往info对象中添加用户信息,token字符串
return info;
}
}
刷新令牌的新机制
为什么刷新令牌?
令牌生成就保存到客户端,即使用户一直使用系统们也不会重新生成令牌,令牌过期,用户必须重新登陆,令牌应该自动续期
解决办法:
1).双令牌机制
设置长短日期的令牌,短时期令牌生效就用长令牌
2).缓存令牌机制
令牌缓存到redis上面,缓存的令牌过期时间是客户端令牌的一倍,
如果客户端令牌过期,缓存令牌没有过期,则生成新的令牌,
如果客户端令牌过期,缓存令牌也过期,则需要重新登陆
客户端如何更新令牌?
如何在响应当中添加令牌?
创建存储令牌的媒介类
//媒介类
@Component
public class ThreadLocalToken {
private ThreadLocal<String> local=new ThreadLocal();
public void setToken(String token){
local.set(token);
}
public String getToken(){
return local.get();
}
public void clear(){
local.remove();
}
}
创建过滤器
OAuth2Filter类,判断那些那些请求应该被shiro处理,如果是options请求直接放行,提交application/json数据,请求被差分成option和post两次,其余所有请求都要被shiro处理。
判断用户Token是真过期还是假过期,真过期,返回提示信息,让用于重新登录,假的过期,就生成新的令牌,返回客户端。
存储新令牌,ThreadLocalToken和redis
OAuth2Filter类
@Component
//创建多例对象prototype表示每次获得bean都会生成一个新的对象
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("emos.jwt.cache-expire")
private int cacheExpire;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate redisTemplate;
/*
拦截请求后,用于把令牌字符串封装成令牌对象
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String token=getRequestToken(req);
if(StrUtil.isBlank(token)){
return null;
}
return new OAuth2Token(token);
}
/*
拦截请求,判断请求是否需要被shiro处理
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
/*
被拦截之后执行
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req=(HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 响应字符集
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
// 允许跨域请求
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
// 请求threadlocal
threadLocalToken.clear();
String token=getRequestToken(req);
if (StrUtil.isBlank(token)){
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效令牌");
return false;
}
try {
jwtUtil.verifierToken(token);
} catch (TokenExpiredException e) {
if(redisTemplate.hasKey(token)){
redisTemplate.delete(token);
int userId = jwtUtil.getUserId(token);
token = jwtUtil.createToken(userId);
redisTemplate.opsForValue().set(token,userId+"",cacheExpire ,TimeUnit.DAYS);
threadLocalToken.setToken(token);
}else {
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("令牌已过期");
return false;
}
}catch (JWTDecodeException e){
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("伪造无效的令牌");
return false;
}
// 间接调用AuthorizingRealm类,认证和授权
boolean bool = executeLogin(request, response);
return bool;
}
//登录失败返回认证消息
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req=(HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 响应字符集
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
// 允许跨域请求
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
try {
resp.getWriter().print(e.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
private String getRequestToken(HttpServletRequest request){
String token=request.getHeader("token");
if(StrUtil.isBlank(token)){
token = request.getParameter("token");
}
return token;
}
}
创建ShiroConfig
把Filter和Realm添加到Shiro框架
创建四个对象返回给springboot
1.SecurityManager 用于封装realm对象
2.ShiroFilterFactoryBean 1).用于封装Filter对象 2).设置Filter拦截路径
3.LifecycleBeanPostProcessor 管理shro对象生命周期
4.AuthorizationAttributeSourceAdvsor 1).Aop切面类 2).web方法执行前,权限验证
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.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class shiroConfig {
// 封装Realm对象
@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setRememberMeManager(null);
return securityManager;
}
// 用于封装Filter对象
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirofilter(SecurityManager securityManager,OAuth2Filter filter){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// oauth过滤
Map<String, Filter> map = new HashMap<>();
map.put("oauth2",filter);
shiroFilter.setFilters(map);
// 设置Filter拦截路径
// 如果是anon表示不需要拦截
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/test/**", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
// 管理shiro的生命周期
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
// @Bean不写默认也是类的首字母小写
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
创建AOP切面类
拦截所有web方法返回值
判断是否刷新生成新令牌
1)检查ThreadLocal中是否保存令牌
2)把新的令牌保存到R对象中
@Aspect
@Component
public class TokenAspect {
@Autowired
private ThreadLocalToken threadLocalToken;
@Pointcut("execution(public * com.qing.emos.wx.controller.*.*(..)))")
public void aspect() {
}
@Around("aspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取拦截对象返回的执行结果
R r=(R)point.proceed();
String token = threadLocalToken.getToken();
if(token!=null){
r.put("token",token);
threadLocalToken.clear();
}
return r;
}
}