本篇主要讲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的用户