本篇主要讲shiro的ShiroConfig配置类,自定义Realm和自定义SessionManager的代码编写
先搭建好springboot+druid+mybatis的环境,相关的dao数据访问层,service层,controller层,pojo类都已经省略
环境的搭建可参考博文
使用缓存的作用
授权的时候每次都去查询数据库,对于频繁访问的接口,性能和响应速度比较慢,所以使用缓存
session持久化的作用
1 假如使用nginx对多个tomcat 进行负载均衡,其实进行负载均衡之前还有一个问题没有解决,那就是集群间的session共享,不然用户在登录网站之后session保存在tomcat A,但是下次访问的时候nginx分发到了tomcat B,这个时候tomcat B没有刚刚用户登录的session,所以用户就失去了(本次)登录状态,下次访问的时候nginx可能又分发到了tomcat A(其实通过配置可以给各个服务器分配权重,nginx根据权重来转发到对应的服务器),用户本次又是登录的状态了,这样飘忽不定肯定是不行的,所以在进行集群负载均衡之前需要解决session共享的问题。
2 重启应用,用户无感知,可以继续以原先的状态继续访问,例如用户正在编辑一篇文章,正要提交修改的时候,但是应用重启了,用户白编辑了半天,需要重新登陆重新编辑,那肯定不行
配置流程和思路
shiroFilterFactoryBean-》
SecurityManager-》
CustomSessionManager-》redisSessionDAO-》CustomSessionIdGenerator
cacheManager
CustomRealm-》hashedCredentialsMatcher
SessionManager
DefaultSessionManager: 默认实现,常用于javase
ServletContainerSessionManager: web环境
DefaultWebSessionManager:常用于自定义实现
1 添加依赖
<!--spring整合shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
2 编写自定义Realm类 CustomRealm
import net.ali.rbac_shiro.domain.Permission;
import net.ali.rbac_shiro.domain.Role;
import net.ali.rbac_shiro.domain.User;
import net.ali.rbac_shiro.service.UserService;
import org.apache.ibatis.annotations.Param;
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 java.util.ArrayList;
import java.util.List;
/**
* 自定义realm
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 进行权限校验的时候回调用
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权 doGetAuthorizationInfo");
// 从 principals获取主身份信息
// 将getPrimaryPrincipal方法返回值转为真实身份类型
//(在上边的doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中身份类型)
User newUser = (User)principals.getPrimaryPrincipal();
//从数据库中查询用户的信息pojo
User user = userService.findAllUserInfoByUsername(newUser.getUsername());
//两个变量用于保存 该用户的角色 和 权限列表
List<String> stringRoleList = new ArrayList<>();
List<String> stringPermissionList = new ArrayList<>();
//这里的代码只是参考,最终要得到用户的角色列表和权限列表即可
List<Role> roleList = user.getRoleList();
for(Role role : roleList){
stringRoleList.add(role.getName());
List<Permission> permissionList = role.getPermissionList();
for(Permission p: permissionList){
if(p!=null){
stringPermissionList.add(p.getName());
}
}
}
//将用户所包含的角色和权限列表返回
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(stringRoleList);
simpleAuthorizationInfo.addStringPermissions(stringPermissionList);
return simpleAuthorizationInfo;
}
/**
* 用户登录的时候会调用
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证 doGetAuthenticationInfo");
//从token获取用户信息,token代表用户输入
String username = (String)token.getPrincipal();
//从数据库中查询此用户
User user = userService.findAllUserInfoByUsername(username);
//从数据库中查询,取密码,用于判断用户名和密码是否正确
String pwd = user.getPassword();
if(pwd == null || "".equals(pwd)){
return null;
}
//第一个参数,集成redis时,这里必须为对象,确保key唯一,且pojo实现序列化
//第二个参数注意,是数据库中的密码
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
}
3 编写shiro的核心类ShiroConfig
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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 java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//必须设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
shiroFilterFactoryBean.setLoginUrl("/pub/need_login");
//登录成功,跳转url,如果前后端分离,则没这个调用
shiroFilterFactoryBean.setSuccessUrl("/");
//没有权限,未授权就会调用此方法, 先验证登录-》再验证是否有权限
shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");
//拦截器路径,坑一,部分路径无法进行拦截,时有时无;因为同学使用的是hashmap, 无序的,应该改为LinkedHashMap
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//退出过滤器
filterChainDefinitionMap.put("/logout","logout");
//匿名可以访问,也是就游客模式
filterChainDefinitionMap.put("/pub/**","anon");
//登录用户才可以访问
filterChainDefinitionMap.put("/authc/**","authc");
//管理员角色才可以访问
filterChainDefinitionMap.put("/admin/**","roles[admin]");
//有编辑权限才可以访问
filterChainDefinitionMap.put("/video/update","perms[video_update]");
//坑二: 过滤链是顺序执行,从上而下,一般讲/** 放到最下面
//authc : url定义必须通过认证才可以访问
//anon : url可以匿名访问
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//如果不是前后端分离,则不必设置下面的sessionManager
securityManager.setSessionManager(sessionManager());
//使用自定义的cacheManager
securityManager.setCacheManager(cacheManager());
//设置realm(推荐放到最后,不然某些情况会不生效)
securityManager.setRealm(customRealm());
return securityManager;
}
/**
* 自定义realm
* @return
*/
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm = new CustomRealm();
//密码加解密规则
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
/**
* 密码加解密规则
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置散列算法:这里使用的MD5算法
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数,好比散列2次,相当于md5(md5(xxxx))
credentialsMatcher.setHashIterations(2);
return credentialsMatcher;
}
//自定义sessionManager
@Bean
public SessionManager sessionManager(){
CustomSessionManager customSessionManager = new CustomSessionManager();
//超时时间,默认 30分钟,会话超时;方法里面的单位是毫秒
//customSessionManager.setGlobalSessionTimeout(20000);
//配置session持久化
customSessionManager.setSessionDAO(redisSessionDAO());
return customSessionManager;
}
/**
* 配置redisManager
*/
public RedisManager getRedisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost");
redisManager.setPassword("123456");
redisManager.setPort(6379);
return redisManager;
}
/**
* 配置具体cache实现类
* @return
*/
public RedisCacheManager cacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(getRedisManager());
//设置过期时间,单位是秒,20s
redisCacheManager.setExpire(20);
return redisCacheManager;
}
/**
* 自定义session持久化
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(getRedisManager());
//设置sessionid生成器
redisSessionDAO.setSessionIdGenerator(new CustomSessionIdGenerator());
return redisSessionDAO;
}
/**
* 管理shiro一些bean的生命周期 即bean初始化 与销毁
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* api controller 层面
* 加入注解的使用,不加入这个AOP注解不生效(shiro的注解 例如 @RequiresGuest)
*
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/**
* 用来扫描上下文寻找所有的Advistor(通知器),
* 将符合条件的Advisor应用到切入点的Bean中,需要在LifecycleBeanPostProcessor创建后才可以创建
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
}
4 编写 自定义SessionManager类
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
//前后端分离,必须要自定义SessionManager
//传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,
//在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,
//因此需要重写shiro获取sessionId的方式。
public class CustomSessionManager extends DefaultWebSessionManager {
//postman测试时,header中提交的token的key名
private static final String AUTHORIZATION = "token";
//继承父类时通常都加上
public CustomSessionManager(){
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if(sessionId != null){
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
//automatically mark it valid here. If it is invalid, the
//onUnknownSession method below will be invoked and we'll remove the attribute at that time.
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}else {
return super.getSessionId(request,response);
}
}
}
5 编写自定义生成sessionid的类CustomSessionIdGenerator
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import java.io.Serializable;
import java.util.UUID;
/**
* 自定义sesionid生成
*/
public class CustomSessionIdGenerator implements SessionIdGenerator {
@Override
public Serializable generateId(Session session) {
return "taobao"+UUID.randomUUID().toString().replace("-","");
}
}
6 编写一个controller,写下登陆的逻辑
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("pub")
public class PublicController {
@RequestMapping("need_login")
public JsonData needLogin(){
return JsonData.buildSuccess("温馨提示:请使用对应的账号登录",-2);
}
@RequestMapping("not_permit")
public JsonData notPermit(){
return JsonData.buildSuccess("温馨提示:拒绝访问,没权限",-3);
}
@RequestMapping("index")
public JsonData index(){
List<String> videoList = new ArrayList<>();
videoList.add("Mysql零基础");
videoList.add("微服务实战");
return JsonData.buildSuccess(videoList);
}
/**
* 登录接口
* @param userQuery
* @param request
* @param response
* @return
*/
@PostMapping("login")
public JsonData login(@RequestBody UserQuery userQuery, HttpServletRequest request, HttpServletResponse response){
Subject subject = SecurityUtils.getSubject();
Map<String,Object> info = new HashMap<>();
try {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userQuery.getName(), userQuery.getPwd());
subject.login(usernamePasswordToken);
info.put("msg","登录成功");
//返回登陆成功后生成的token,这是关键
info.put("session_id", subject.getSession().getId());
return JsonData.buildSuccess(info);
}catch (Exception e){
e.printStackTrace();
return JsonData.buildError("账号或者密码错误");
}
}
}
经过以上的步骤,就可以写些其它的controller用postman做测试了
需要注意或需要改进的地方
1 session的超时时间默认是30钟,做测试时可以改短点,注意:在限定的时间内,只要一直在操作,session就会一直有限
2 关于退出功能,shiro已经帮我们开发好,直接使用即可,url在ShiroConfig类中配置,例:http://localhost:8080/logout,使用postman测试,header中把token加上,就会退出当前token的用户