1 介绍 :背景.....发展.....此处省略废话一万句
有啥用?: 鉴权框架
为啥用他?:相比Spring家族的鉴权系统,shiro是轻量级的鉴权框架,可做基于session的有状态登录,基于token无状态的登录的,底层是一串的过滤器链呗
注意点: shiro底层还是依赖Servlet,不能和SpringCloudGateWay集成
2 缓存问题:
shiro本身不实现cache,但是对cache做了抽象,使用缓存需要自身实现
3 常用场景:
单体架构:MD5,redis,依赖sessionn状态 微服务: 单点登录,jwt,redis,md5,关闭session,没次登录都做拦截,检验token信息
4 依赖注解对接口做鉴权:
/** * @RequiresAuthentication * 表示当前Subject已经通过login 进行了身份验证 * <p> * @RequiresUser * 表示当前Subject已经身份验证或者通过记住我登录的。 * <p> * @RequiresGuest * 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。 * <p> * @RequiresRoles 角色认证 * <p> * @RequiresPermissions 验证权限的注解 */
5 demo:
shiro导包问题
配置ShiroFilterFactorBean 导入 <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.4.2</version> </dependency> 配置 ShiroFilterChainDefinition <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.4.0-RC2</version> </dependency>
本次只做了基于token配合Redis的无状态登录案例:
(1) 签发token
package com.example.newshiro202304;
import com.example.shiro.utils.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class Controller {
/**
* 主要是做密码和用户的验证,然后签发token
* (1) 验证密码
* (2) 检验redis中当前账号的存储个数,限制登录人数
* (3) 将token和ip绑定,防止token被挟持
* (4) 签发token
* @param password
* @param account
* @param request
* @return
*/
@RequestMapping("/login")
public Object login(String password, String account, HttpServletRequest request) {
User user = mapper.select(account) ;
//检验密码,将密码加密
if(!uset.getPassword.equals(Md5.jiami(password))){
throw new Exception("密码不对");
}
//在redis中获取当前已经登录的人员的数量,根据账号生成和的token判断
int num=redisUtils.getprefix(account+"token").size();
if (num>3){
throw new Exception("登录人数限制为3");
}
//生成token
final String token = JwtUtils.getToken(account, account);
//将token存在redis,同时将对应的IP也存在redis中,设置过期时间为30min
redisUtils.set(token+"ip",request.getRemoteHost(),30);
redisUtils.set(token,token,30);
//签发token
return token;
}
}
(2) token:
package com.example.newshiro202304;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken() {
}
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
(3)redis配置:
package com.example.newshiro202304;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;//LettuceConnectionFactory 其实就是这个
@Bean
public RedisTemplate<String,Object> redisTemplate(){
final RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new JdkSerializationRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
(4) shiro 的基本配置:
package com.example.newshiro202304;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.cache.RedisCacheManager;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.logging.Logger;
@Configuration
public class ShiroConfig {
//private static final Logger log= LoggerFactory.getLogger(ShiroConfig.class);
@Autowired
private Environment env;
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
final ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
final HashMap<String, Filter> map = new HashMap(1);
//map.put("jwt",new JwtFilter());
shiroFilterFactoryBean.setFilters(map);
final LinkedHashMap<String, String> lMap = new LinkedHashMap<>();
lMap.put("/login", "anno");
lMap.put("/get", "user");
//lMap.put("/**", "atuhc");//一般采用自定义的,单个服务为不涉token共享时可用
lMap.put("/**", "jwt");//集群环境下需要自己设置过滤规则
//lMap.put("/**", "jwt[manger,user]");//集群环境下需要自己编写过滤器,参数对应过去其中的Object mappedValue字符串数组,检测实体类中的第一页个集合(权限或者角色)
shiroFilterFactoryBean.setLoginUrl("/login");//设置登录接口
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");//未授权错误页面
shiroFilterFactoryBean.setFilterChainDefinitionMap(lMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager(ShiroRelm relm) {
final DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(relm);
//关闭自带的session,强制每次登录都做检测
final DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
final DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(defaultSubjectDAO);
//自定义缓存实现
//defaultWebSecurityManager.setCacheManager(new RedisCacheManager());
//自定义sessio管理
//defaultWebSecurityManager.setSessionManager(new SessionManager());
//引入两种身份验证
// SecurityUtils.setSecurityManager(defaultWebSecurityManager);
//多个Relam认证策略
//defaultWebSecurityManager.setRealms();
//多Relm的设置认证策略设置
//defaultWebSecurityManager.setAuthenticator();
return defaultWebSecurityManager;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
final AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 解决引入AOP后controller中加入角色检验注解导致接口映射失效
*
* @return
*/
//@Bean
// @DependsOn("lifeBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
final DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
// @Bean
//public RedisCacheManager redisCacheManager() {
// final RedisCacheManager redisCacheManager = new RedisCacheManager();
// //这个RedisCacheManager导报有问题
// //redisCacheManager.setRedisManger(redismanger)
// //shiro-redis需要一个id字段来标识在redis中的授权对象,其实就是为了缓存对象,new SimpleAuthenticationInfo(loginuser,token,getName())
// // redisCacheManager.setPrincipalIdFieldName("id");
// //设置缓存失效时间
// // redisCacheManager.setExpire(300000);
// return redisCacheManager;
//}
//@Bean
//public RedisManger redisManger(){
// RedisManger redisManger=new RedisManger();
// //todo,将redis的链接信息设置
// return redisManger;
//}
}
(5) realm,鉴权流程就是经过这个做的映射:
package com.example.newshiro202304;
import com.example.shiro.utils.JwtUtils;
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 java.util.HashSet;
import java.util.List;
public class ShiroRealm extends AuthorizingRealm {
public ShiroRealm() {
}
/**
* 自定义的token需要重写
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 鉴权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (principalCollection == null) {
return info;
}
final User user = (User) principalCollection.getPrimaryPrincipal();
List<String> roles = mapper.selectRoles(user.getID);
List<String> licendes = mapper.selectLicendes(user.getID);
//将角色和权限信息刷到对象中
info.setRoles(new HashSet<>(roles));
info.setObjectPermissions(new HashSet(licendes));
return info;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
if (token == null) {
throw new AuthenticationException();
}
User user = checkToken(token);
final SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, token, getName());
return simpleAuthenticationInfo;
}
private User checkToken(String token) {
final String account = JwtUtils.getAccount(token);
if (account == null) {
throw new AuthenticationException();
}
//检测token
String rtoken = redisUtils.get(token);
String ip = redisUtils.get(token + "ip");
if (rtoken == null | !request.getIP.equals(ip)) {
//token失效或者被挟持
throw new AuthenticationException();
}
//续约token
JwtTokenRefresh(token, rtoken, account,ip);
//从数据库或者reis缓存中查询redis,这一步步自己实现
return user;
}
private void JwtTokenRefresh(String token, String rtoken, String account,String ip) {
//jwt生成的token有效是十五分钟,但是redis存储是半个小时,这样每次检查都是redis 中的token
if (JwtUtils.check(rtoken, account, account)) {
//redis中的token还能用,
return;
}
//重新生成token,续约
final String newtoken = JwtUtils.getToken(account, account);
redisUtils.set(token, newtoken,30);
redisUtils.set(token+"ip",ip,30);
}
}
(6) 自定义过滤器:
package com.example.newshiro202304;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
/***
* 这个类不能添加Spring的注解,否则无法加载
*/
public class JwtFilter extends BasicHttpAuthenticationFilter {
@SneakyThrows
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
//token认证
executeLogin(request, response);
//鉴权
return checkRoleOrPrim(request, response, mappedValue);
} catch (Exception e) {
//登录失败,鉴权失败
final HttpServletResponse response1 = (HttpServletResponse) response;
response1.setHeader("Context-type", "application/json;charset=UTF-8");
final HashMap<String, String> data = new HashMap<>();
data.put("data", "");
data.put("code", "403");
data.put("msg", "无权限");
final String s = JSONObject.toJSONString(data);
response1.getWriter().write(s);
return false;
}
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest request1 = (HttpServletRequest) request;
final String header = request1.getHeader("X-Access-Token");
final JwtToken jwtToken = new JwtToken(header);
Subject subject = SecurityUtils.getSubject();
subject.login(jwtToken);
return true;
}
private boolean checkRoleOrPrim(ServletRequest request, ServletResponse response, Object mappedValue) {
final Subject subject = getSubject(request, response);
//可以转换为用户对象
//final Object principal = subject.getPrincipal();
//获取的是对象中的第一个集合
final PrincipalCollection principals = subject.getPrincipals();
//(String[])mappedValue为用户权限
return subject.isPermittedAll((String[]) mappedValue);
}
/**
* token超时
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//saveRequest(request);//保存请求,重新登录后跳转到
//redirectToLogin(request,response);重定向到登录页面
return false;
}
//处理跨域
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest request1 = (HttpServletRequest) request;
HttpServletResponse response1 = (HttpServletResponse) response;
response1.setHeader("Access-Control-Allow-Origin", request1.getHeader("Origin"));
response1.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
response1.setHeader("Access-Control-Allow-Headers", request1.getHeader("Access-Control-Request-Headers"));
if (request1.getMethod().equals("OPTIONS")) {
response1.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
注意对跨域做支持: options跨域请求不会先发送options,没有带cookie信心,在过滤
(7) 鉴权接口:
package com.example.shiro.controller;
import com.example.shiro.entity.SysUser;
import com.example.shiro.mapper.UserMapper;
import com.example.shiro.realm.old.Redis;
import com.example.shiro.utils.JwtUtils;
import com.example.shiro.utils.RedisUtils;
import com.example.shiro.utils.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.repository.query.Param;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.ServletResponse;
import java.util.Map;
/**
* @RequiresAuthentication
* 表示当前Subject已经通过login 进行了身份验证
* <p>
* @RequiresUser
* 表示当前Subject已经身份验证或者通过记住我登录的。
* <p>
* @RequiresGuest
* 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
* <p>
* @RequiresRoles 角色认证
* <p>
* @RequiresPermissions 验证权限的注解
*/
@Slf4j
@RestController
public class ShiroController {
@Autowired
private UserMapper mapper;
@Autowired
private RedisUtils redisUtils;
//@GetMapping("login")
public Object login(String account, String password) {
log.info("login account is :{}", account);
log.info("login password is :{}", password);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(account, password);
subject.login(token);
return "login";
}
@PostMapping("login")
public void JwtLogin(@RequestBody Map map) {
log.info("login account and pass is :{}", map);
final String account = (String) map.get("account");
final String password = (String) map.get("password");
final SysUser userInfo = mapper.getUserInfo(Long.valueOf(account));
if (userInfo == null || !userInfo.getPassword().equals(password)) {
throw new AccountException("account is mess");
}
final String token = JwtUtils.getToken(account, password);
SpringUtil.getResponse().setHeader("X-Access_Token", token);
redisUtils.set("UserToken_"+account+token,token,1800L);
redisUtils.set("UserUrl_"+account+token,SpringUtil.getHostMsg(),1800L);
}
@RequiresRoles("manger")
@GetMapping("role")
public Object role() {
return "role";
}
@RequiresPermissions("insert")
@GetMapping("permissions")
public Object permissions() {
return "permissions";
}
@RequiresRoles("manger")
@RequiresPermissions("insert")
@GetMapping("all")
public Object roleAndPermissions() {
return "roleAndPermissions";
}
@RequiresPermissions("delect")
@GetMapping("err")
public Object err_test() {
return "eer===";
}
@Resource(name = "redisTemplate", type = RedisTemplate.class)
private RedisTemplate template;
@GetMapping("redis")
public void getMsg() {
template.opsForValue().set("123456", 123456);
}
}