spring boot结合shiro实现用户-角色-权限的控制(包含用户名密码登陆和手机号验证码登陆)

本文介绍如何在SpringBoot项目中整合Shiro框架,实现基于角色和权限的用户认证与授权。涵盖依赖引入、配置文件设置、自定义Realm、动态权限管理及密码比较器的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

spring boot整合shiro实现权限校验

1.首先导入项目所需jar包

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
        <mysql.version>5.1.47</mysql.version>
        <mybaitsPlus.version>3.1.1</mybaitsPlus.version>
        <druid.version>1.0.31</druid.version>
        <shiro-redis.version>3.1.0</shiro-redis.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--devtools热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>runtime</scope>
        </dependency>

        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!--redis存储-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
            <scope>compile</scope>
        </dependency>
        <!-- Shiro-redis插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>${shiro-redis.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>
        <dependency>
			<groupId>com.sun</groupId>
			<artifactId>tools</artifactId>
			<version>1.8.0</version>
			<scope>system</scope>
			<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
			<optional>true</optional>
		</dependency>
        <!-- 分页插件 -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>

2. 编写yml和properties配置文件

yml文件

server:
  port: 8083
  #tomcat:
  #max-http-post-size: -1
  max-http-header-size: 4048576
spring:
  profiles:
    active: diagnose
  application:
    name: user-center
  devtools:
    restart:
      enabled: true  #   设置开启热部署
      additional-paths: src/main/java #  重启目录
      exclude: WEB-INF/**
  freemarker:
    cache: false    #  页面不加载缓存,修改即时生效
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.1.199/zhzd?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

#  myBatis-plus
mybatis-plus:
  #configuration:
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #  输出sql日志和查询结果日志
  mapper-locations: classpath*:mapper/**Mapper.xml     #  .xml目录
  type-aliases-package: com.etouch.mapper          #  接口包
  global-config:
    db-config:
      db-type: mysql
      column-underline=true: #  默认驼峰

# 只打印sql日志和sql参数
logging:
  file: logs/sys.log
  level:
    com:
      etouch:
        mapper: debug

# shiro自定义配置
shiro:
  session:
    jsessionid: x-auth-token
  role:
    superRole: superAdmin

---
#   Redis数据源
spring:
  profiles: diagnose
  redis:
    host: localhost
    port: 6379
    timeout: 6000
    password: null
    database: 0
    jedis:
      pool:
        max-active: 1000  #   连接池最大连接数(使用负值表示没有限制)
        max-wait: -1      #   连接池最大阻塞等待时间(使用负值表示没有限制)
        max-idle: 10      #   连接池中的最大空闲连接
        min-idle: 5       #   连接池中的最小空闲连接
eureka:
  instance:
    prefer-ip-address: true
    instance-id: user-center
    lease-expiration-duration-in-seconds: 15
    lease-renewal-interval-in-seconds: 5
  client:
    service-url:
      defaultZone:  http://eureka:eureka@localhost:8800/eureka/
    registry-fetch-interval-seconds: 5

properties文件


#redis 中存储的用户登录数据结构
user.login.redis=zhzd:user:login:
#redis 中存储的验证码结构
user.send.code=zhzd:user:code:
#redis 中存储的用户信息
user.info.dto=zhzd:user:login:dto:

3.新增MyShiroRealm继承AuthorizingRealm,

package com.etouch.config.shiro;

import com.etouch.DTO.MenuDTO;
import com.etouch.DTO.RoleDTO;
import com.etouch.DTO.UserDTO;
import com.etouch.corecenter.utils.UuidUtils;
import com.etouch.entity.SysMenu;
import com.etouch.entity.SysRole;
import com.etouch.entity.SysUser;
import com.etouch.entity.SysUserToken;
import com.etouch.service.SysUserTokenService;
import com.etouch.service.UserService;
import com.etouch.utils.ShiroUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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;

public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    /**
     * 权限校验
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserDTO user = (UserDTO) SecurityUtils.getSubject().getPrincipal();
        UserDTO userDTO = userService.findUserByName(user.getLoginName());
        for (RoleDTO role : userDTO.getRoleList()) {
            authorizationInfo.addRole(role.getRoleName());
            for (MenuDTO menu : role.getMenuDTOList()) {
                authorizationInfo.addStringPermission(menu.getMenuUrl());
            }
        }
        return authorizationInfo;
    }

    /**
     * 登录认证
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //强转成自己传入的参数,要不然获取不到自己传入的用户名和密码
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //获得前台输入的用户名和密码
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());
        //根据邮箱获得数据库的用户
        UserDTO user = userService.findUserByName(username);
        if (user == null) {
            return null;  //return null 在底层会抛出异常
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        return info;
    }

  /*  // 清除缓存
    public void clearCached() {
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }*/

4. 新增动态获取权限, 校验权限业务类

接口类

package com.etouch.config.shiro.service;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;

import java.util.Map;

/**
 *  <p> shiro权限处理 </p>
 */
public interface ShiroService {

    /**
     * 初始化权限 -> 拿全部权限
     *
     * @param :
     * @return: java.util.Map<java.lang.String,java.lang.String>
     */
    Map<String, String> loadFilterChainDefinitionMap();

    /**
     * 在对uri权限进行增删改操作时,需要调用此方法进行动态刷新加载数据库中的uri权限
     *
     * @param shiroFilterFactoryBean
     * @param roleId
     * @param isRemoveSession:
     * @return: void
     */
    void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, String roleId, Boolean isRemoveSession);

    /**
     * shiro动态权限加载 -> 原理:删除shiro缓存,重新执行doGetAuthorizationInfo方法授权角色和权限
     *
     * @param roleId
     * @param isRemoveSession:
     * @return: void
     */
    void updatePermissionByRoleId(String roleId, Boolean isRemoveSession);

}

实现类

package com.etouch.config.shiro.service.impl;

import com.etouch.config.shiro.service.ShiroService;
import com.etouch.entity.SysMenu;
import com.etouch.entity.SysUser;
import com.etouch.exception.ZhException;
import com.etouch.mapper.SysMenuMapper;
import com.etouch.mapper.SysRoleMapper;
import com.etouch.mapper.SysUserMapper;
import com.etouch.utils.ShiroUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * <p> shiro权限处理实现类 </p>
 *
 * @description:
 * @author: zhengqing
 * @date: 2019/9/7 0007 13:53
 */
@Slf4j
@Service
public class ShiroServiceImpl implements ShiroService {

    @Autowired
    private SysMenuMapper menuMapper;
    @Autowired
    private SysUserMapper userMapper;

    @Autowired

    @Override
    public Map<String, String> loadFilterChainDefinitionMap() {
        // 权限控制map
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置过滤:不会被拦截的链接 -> 放行 start ----------------------------------------------------------
        // 放行Swagger2页面,需要放行这些
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");

        // 三方登录
//        filterChainDefinitionMap.put("/auth/loginByQQ", "anon");
//        filterChainDefinitionMap.put("/auth/afterlogin.do", "anon");
        // 退出
        // token过期接口
        filterChainDefinitionMap.put("/sysUser/tokenExpired", "anon");
        // 被挤下线
        filterChainDefinitionMap.put("/sysUser/downline", "anon");
        //手机号,验证码登录
        filterChainDefinitionMap.put("/sysUser/login", "anon");
        filterChainDefinitionMap.put("/sysUser/loginByPhone", "anon");
        filterChainDefinitionMap.put("/sysUser/logOut", "anon");

        //管理员可操作所有的
        //发送验证码和校验验证码
        filterChainDefinitionMap.put("/*/sendCode", "anon");
        filterChainDefinitionMap.put("/*/checkoutCode", "anon");
        //swaggerUI
        // 放行 end ----------------------------------------------------------
        //TODO 管理员可以访问任何接口
        filterChainDefinitionMap.put("/**", "roles[superAdmin]");
        // 从数据库或缓存中查取出来的url与resources对应则不会被拦截 放行
        List<SysMenu> permissionList = menuMapper.selectList(null);
        if (!CollectionUtils.isEmpty(permissionList)) {
            permissionList.forEach(e -> {
                if (StringUtils.isNotBlank(e.getMenuUrl())) {
                    filterChainDefinitionMap.put(e.getMenuUrl(), "perms["+e.getMenuUrl()+"]");
//                        filterChainDefinitionMap.put("/api/system/user/listPage", "authc,token,zqPerms[user1]"); // 写死的一种用法
                }
            });
        }
        // ⑤ 认证登录  【注:map不能存放相同key】
        //TODO  开发阶段不需要登录就可以访问
        filterChainDefinitionMap.put("/**", "anon");
        return filterChainDefinitionMap;
    }

    @Override
    public void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, String roleId, Boolean isRemoveSession) {
        synchronized (this) {
            AbstractShiroFilter shiroFilter;
            try {
                shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
            } catch (Exception e) {
                throw new ZhException("get ShiroFilter from shiroFilterFactoryBean error!");
            }
            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
            DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();

            // 清空拦截管理器中的存储
            manager.getFilterChains().clear();
            // 清空拦截工厂中的存储,如果不清空这里,还会把之前的带进去
            //            ps:如果仅仅是更新的话,可以根据这里的 map 遍历数据修改,重新整理好权限再一起添加
            shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
            // 动态查询数据库中所有权限
            shiroFilterFactoryBean.setFilterChainDefinitionMap(loadFilterChainDefinitionMap());
            // 重新构建生成拦截
            Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                manager.createChain(entry.getKey(), entry.getValue());
            }
            log.info("--------------- 动态生成url权限成功! ---------------");

            // 动态更新该角色相关联的用户shiro权限
            if (roleId != null) {
                updatePermissionByRoleId(roleId, isRemoveSession);
            }
        }
    }

    @Override
    public void updatePermissionByRoleId(String roleId, Boolean isRemoveSession) {
        // 查询当前角色的用户shiro缓存信息 -> 实现动态权限
        List<SysUser> userList = userMapper.selectUserByRoleId(roleId);
        // 删除当前角色关联的用户缓存信息,用户再次访问接口时会重新授权 ; isRemoveSession为true时删除Session -> 即强制用户退出
        if (!CollectionUtils.isEmpty(userList)) {
            for (SysUser user : userList) {
                ShiroUtils.deleteCache(user.getLoginName(), isRemoveSession);
            }
        }
        log.info("--------------- 动态修改用户权限成功! ---------------");
    }

}

5. 新建shiro的配置文件ShiroConfig

package com.etouch.config.shiro;

import java.util.ArrayList;
import java.util.List;

import com.etouch.utils.CustomCredentialsMatcher;
import com.etouch.config.shiro.service.impl.ShiroServiceImpl;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

    private final String CACHE_KEY = "shiro:cache:";
    private final String SESSION_KEY = "shiro:session:";
    private final int EXPIRE = 1000*60*60*24*7; //TODO 单位毫秒

    /**
     * Redis配置
     */
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${shiro.session.jsessionid}")
    private String jsessionid;

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, ShiroServiceImpl shiroService) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        /*
        * 常用的过滤器如下:

            authc:所有已登陆用户可访问

            roles:有指定角色的用户可访问,通过[ ]指定具体角色,这里的角色名称与数据库中配置一致

            perms:有指定权限的用户可访问,通过[ ]指定具体权限,这里的权限名称与数据库中配置一致

            anon:所有用户可访问,通常作为指定页面的静态资源时使用
        * */
        //未登录提示
        shiroFilterFactoryBean.setLoginUrl("/sysUser/unLogin");
        //没有权限提示
        shiroFilterFactoryBean.setUnauthorizedUrl("/sysUser/unAuth");
        // 权限控制map.

        shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroService.loadFilterChainDefinitionMap());
        return shiroFilterFactoryBean;
    }

    @Bean
    public CustomCredentialsMatcher credentialMatcher() {
        return new CustomCredentialsMatcher();
    }

    // 将自己的验证方式加入容器,注入自定义的realm
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        //将密码比较器注入自定义realm域
        myShiroRealm.setCredentialsMatcher(credentialMatcher());
        myShiroRealm.setCacheManager(cacheManager());
        return myShiroRealm;
    }

    @Bean
    public PhoneRealm phoneRealm() {
        PhoneRealm phoneRealm = new PhoneRealm();
        phoneRealm.setCacheManager(cacheManager());
        return phoneRealm;
    }

    // 权限管理,配置主要是Realm的管理认证

    /**
     * 配置核心安全事务管理器
     *
     * @param shiroRealm
     * @return
     */
    @Bean(name = "securityManager")
    public SecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm shiroRealm, PhoneRealm phoneRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realms
        List<Realm> realms = new ArrayList<>();
        realms.add(shiroRealm);
        realms.add(phoneRealm);
        //设置自定义realm.
        securityManager.setRealms(realms);
        //配置记住我
        securityManager.setRememberMeManager(rememberMeManager());
        // 自定义session管理
        securityManager.setSessionManager(sessionManager());
        // 自定义Cache实现缓存管理
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

    /**
     * cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe,自定义
     *
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:

        //setcookie()的第七个参数
        //设为true后,只能通过http访问,javascript无法访问
        //防止xss读取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }

    /**
     * cookie管理对象;记住我功能,rememberMe管理器
     *
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }

    // 加入注解的使用,不加入这个注解不生效
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 配置Redis管理器:使用的是shiro-redis开源插件
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
//        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * 配置Cache管理器:用于往Redis存储权限和角色标识  (使用的是shiro-redis开源插件)
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // 配置缓存的话要求放在session里面的实体类必须有个id标识 注:这里id为用户表中的主键,否-> 报:User must has getter for field: xx
        redisCacheManager.setPrincipalIdFieldName("id");
        return redisCacheManager;
    }

    /**
     * SessionID生成器
     */
    @Bean
    public ShiroSessionIdGenerator sessionIdGenerator() {
        return new ShiroSessionIdGenerator();
    }

    /**
     * 配置RedisSessionDAO (使用的是shiro-redis开源插件)
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setKeyPrefix(SESSION_KEY);
        redisSessionDAO.setExpire(EXPIRE);
        return redisSessionDAO;
    }

    /**
     * 配置Session管理器
     */
    @Bean
    public SessionManager sessionManager() {
        ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
        shiroSessionManager.setGlobalSessionTimeout(EXPIRE);
        shiroSessionManager.setSessionDAO(redisSessionDAO());//修改默认session存储机制,将shiro中的session数据存储至redis数据库中
        shiroSessionManager.setSessionIdCookie(getSessionIdCookie());//设置sessionId的Key
        shiroSessionManager.setDeleteInvalidSessions(true);//自动清空失效session
        return shiroSessionManager;
    }

    /**
     * 给shiro的sessionId默认的JSSESSIONID名字改掉
     * @return
     */
    @Bean(name="sessionIdCookie")
    public SimpleCookie getSessionIdCookie(){
        SimpleCookie simpleCookie = new SimpleCookie(jsessionid);
        return simpleCookie;
    }

    /**
     * 该类如果不设置为static,@Value注解就无效,原因未知
     * @return
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

}
自定义密码比较器
package com.etouch.utils;

import com.etouch.corecenter.utils.MD5Util;
import com.etouch.utils.Encrypt;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;

public class CustomCredentialsMatcher implements CredentialsMatcher {
    /**
     *
     * @param token 用户输入的用户名和密码
     * @param info  数据库中的用户名和密码
     * @return  返回如果是false 则登录失败, 返回true 则登录成功
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info)  {
        /**
         * 验证数据库的密码 和 用户输入的密码 是否一致
         *
         * @param token 用户输入的用户名 密码
         * @param info  数据库中的用户名 密码
         * @return 返回值 比较密码是否正确 比较正确 返回true 表示登陆成功 返回false 表示登陆失败(同时抛出异常)
         *
         * 明文密码: 不加密 原始密码
         * 密文密码: 经过一定的加密处理 , 数据更加安全
         * md5加密 : 不可逆的加密方式
         * 先将加密后的数据 以及加密前的数据 提前保存了 根据对应的数据找即可
         * 123456 ->  xxxxyyyyqqqq
         *
         * 如何屏蔽这种情况
         * 123456 ->  xxxxyyyyqqqq
         * 123456000 ->  asdasdfg
         * 用户输入的密码 + 固定值(不变的值) =  特殊的密码
         * 用户输入的密码 + 可变化的固定值 =  特殊的密码
         * password:123456 + 用户的登录名 = 特殊的密码
         * 加密: 直接转换
         * 加盐加密: 在原有的密码基础上 加上一定的参数 针对该用户而言 这个盐(新加入的参数) 是不变的
         */
       // System.out.println("密码比较器执行");
//        //加盐加密
//        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//        //获取用户输入的数据
//        String email = upToken.getUsername();
//        String password = new String(upToken.getPassword());
//        //对密码进行加密处理  加盐加密
//        String md5Pwd = Encrypt.md5(password, email);
//        //获得数据库的密码
//        String dbPwd = (String) info.getCredentials();
//        return md5Pwd.equals(dbPwd);
        //不加盐加密
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //获得用户输入的密码
        String password = new String(upToken.getPassword());
        //加密后的密码
        String md5Pwd = MD5Util.string2MD5(password);
        //获取数据库的密码
        String dbPwd = (String) info.getCredentials();
        return md5Pwd.equals(dbPwd);
    }
}

### 6. 新建shiro的session管理器

```java

package com.etouch.config.shiro;

import com.etouch.utils.constants.Constants;
import org.apache.commons.lang3.StringUtils;
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;

import static org.apache.shiro.web.servlet.ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE;

/**
 *  <p> 自定义获取Token </p>
 *
 * @description :
 * @author : zhengqing
 * @date : 2019/8/23 15:55
 */
public class ShiroSessionManager extends DefaultWebSessionManager {
    /**
     * 定义常量
     */
//    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";


    /**
     * 重写构造器
     */
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }

    /**
     * 重写方法实现从请求头获取Token便于接口统一
     * 每次请求进来,Shiro会去从请求头找REQUEST_HEADER这个key对应的Value(Token)
     */
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader(Constants.REQUEST_HEADER);

        // 如果请求头中存在token 则从请求头中获取token
        if ( StringUtils.isNotBlank( token ) ) {
            request.setAttribute(REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            // 否则按默认规则从cookie取token
            return super.getSessionId(request, response);
        }
    }
}

7. 用户名密码登陆

  /**
     * 用户名密码登录
     *
     * @param userName
     * @param passWord
     * @param rememberMe
     * @return
     */
    @ApiOperation(value = "用户名密码登录", notes = "用户名密码登录")
    @PostMapping("/login")
    public ResultUtils<String> login(HttpServletRequest request,
                                     @ApiParam(value = "用户名", required = true) @RequestParam(name = "userName") String userName,
                                     @ApiParam(value = "密码", required = true) @RequestParam(name = "passWord") String passWord,
                                     @ApiParam("记住密码,true,false") @RequestParam(name = "rememberMe") boolean rememberMe,
                                     @ApiParam("验证码") @RequestParam(name = "VerificationCode") String VerificationCode) {
        if (StringUtils.isBlank(userName) || StringUtils.isBlank(passWord)) {
            return ResultUtils.error("用户名或密码不能为空");
        }
        if (checkVerify(request, VerificationCode).getCode() != 200) {
            return ResultUtils.errorParam("验证码错误,请重新输入");
        }
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper();
        queryWrapper.eq("login_name", userName);
        SysUser sysUser = sysUserService.getOne(queryWrapper);
        if (sysUser == null) {
            return ResultUtils.error("用户名不存在");
        }
        String encodePwd = MD5Util.string2MD5(passWord);
        log.info("加密码后的密码为: " + encodePwd);
        if (!encodePwd.equals(sysUser.getPassword())) {
            return ResultUtils.error("用户名或密码错误");
        }
        //登录前,先清除掉之前的用户登录信息
        this.shiroCheck();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName, passWord, rememberMe);
        //登录操作
        Subject subject = SecurityUtils.getSubject();
        LoginUser loginUser = new LoginUser();//存入用户id和token
        String uToken = null;
        try {
            //进行shiro登录验证
            subject.login(usernamePasswordToken);
            uToken = ShiroUtils.getSession().getId().toString();
            //subject.getSession().setAttribute("loginUser", sysUser);//
            userService.login(sysUser, rememberMe, uToken);
            SecurityUtils.getSubject().getSession().setTimeout(1000*60*60*24*7);
            return ResultUtils.success("登陆成功", uToken);
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return ResultUtils.error("登录失败");
        } catch (InvalidSessionException e) {
            e.printStackTrace();
            return ResultUtils.error("登录失败");
        }
    }

8 如需增加手机号, 验证码登陆, 则需进行以下配置

8.1重写shiro的token获取类
package com.etouch.config.shiro;

import org.apache.shiro.authc.HostAuthenticationToken;
import org.apache.shiro.authc.RememberMeAuthenticationToken;

import java.io.Serializable;

public class UserNamePasswordPhoneToken implements HostAuthenticationToken, RememberMeAuthenticationToken, Serializable {

    // 手机号码
    private String phone;
    private boolean rememberMe;
    private String host;

    /**
     * 重写getPrincipal方法
     */
    public Object getPrincipal() {
        return phone;
    }

    /**
     * 重写getCredentials方法
     */
    public Object getCredentials() {
        return phone;
    }

    public UserNamePasswordPhoneToken() {
        this.rememberMe = false;
    }

    public UserNamePasswordPhoneToken(String phone) {
        this(phone, false, null);
    }

    public UserNamePasswordPhoneToken(String phone, boolean rememberMe) {
        this(phone, rememberMe, null);
    }

    public UserNamePasswordPhoneToken(String phone, boolean rememberMe, String host) {
        this.phone = phone;
        this.rememberMe = rememberMe;
        this.host = host;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String getHost() {
        return host;
    }

    @Override
    public boolean isRememberMe() {
        return rememberMe;
    }
}


8.2 新建PhoneRealm 继承AuthorizingRealm
package com.etouch.config.shiro;

import com.etouch.DTO.MenuDTO;
import com.etouch.DTO.RoleDTO;
import com.etouch.DTO.UserDTO;
import com.etouch.entity.SysUser;
import com.etouch.DTO.search.SearchUserArgs;
import com.etouch.service.UserService;
import org.apache.shiro.SecurityUtils;
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.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;

public class PhoneRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // 登录认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UserNamePasswordPhoneToken token = null;
        // 如果是PhoneToken,则强转,获取phone;否则不处理。
        if (authenticationToken instanceof UserNamePasswordPhoneToken) {
            token = (UserNamePasswordPhoneToken) authenticationToken;
        } else {
            return null;
        }

        String phone = (String) token.getPrincipal();
        SearchUserArgs searchUserArgs = new SearchUserArgs();
        if (phone.contains("@")) {
            searchUserArgs.setEmail(phone);
        } else {
            searchUserArgs.setPhone(phone);
        }
        SysUser user = userService.findByArgs(searchUserArgs);
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user,userDTO);
        if (userDTO == null) {
            throw null;
        }
        return new SimpleAuthenticationInfo(userDTO, phone, this.getName());
    }

    /**
     * 权限校验
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserDTO user = (UserDTO) SecurityUtils.getSubject().getPrincipal();
        UserDTO userDTO = userService.findUserByName(user.getLoginName());
        for (RoleDTO role : userDTO.getRoleList()) {
            authorizationInfo.addRole(role.getRoleName());
            for (MenuDTO menu : role.getMenuDTOList()) {
                authorizationInfo.addStringPermission(menu.getMenuUrl());
            }
        }
        return authorizationInfo;
    }
    @Override
    public boolean supports(AuthenticationToken var1) {
        return var1 instanceof UserNamePasswordPhoneToken;
    }


}


8.3 新增手机号验证码登陆
 @ApiOperation("手机号,验证码登录")
    @PostMapping("/loginByPhone")
    public ResultUtils<String> login(@ApiParam("手机号") @RequestParam(name = "phone") String phone,
                                     @ApiParam("验证码") @RequestParam(name = "code") String code, @ApiParam("记住密码") Boolean rememberMe) {
        //首先校验验证码
        Boolean b = userService.checkoutCode(phone, code);
        if (!b) {
            return ResultUtils.errorParam("验证码错误,请重新输入");
        }

        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        //通过手机号或者与邮箱账号查询用户
        SearchUserArgs searchUserArgs = new SearchUserArgs();
        if (phone.contains("@")) {
            searchUserArgs.setEmail(phone);
        } else {
            searchUserArgs.setPhone(phone);
        }
        SysUser sysUser = userService.findByArgs(searchUserArgs);
        if (sysUser == null) {
            return ResultUtils.errorParam("查找不到用户信息");
        }
        this.shiroCheck();//清除掉这台客户端的上个用户的登录信息
        UserNamePasswordPhoneToken userNamePasswordPhoneToken = new UserNamePasswordPhoneToken(phone);
        //登录操作
        Subject subject = SecurityUtils.getSubject();
        LoginUser loginUser = new LoginUser();//存入用户id和token
        String uToken = null;
        try {
            //进行shiro登录验证
            uToken = ShiroUtils.getSession().getId().toString();
            subject.login(userNamePasswordPhoneToken);
            userService.login(sysUser, rememberMe, uToken);
            return ResultUtils.success("登陆成功", uToken);
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return ResultUtils.error("登录失败");
        } catch (InvalidSessionException e) {
            e.printStackTrace();
            return ResultUtils.error("登录失败");
        }
    }

ShiroUtils工具类

package com.etouch.utils;

import com.etouch.DTO.UserDTO;
import com.etouch.corecenter.enums.ExceptionEnum;
import com.etouch.corecenter.exception.ProjectExeption;
import com.etouch.entity.SysUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.LogoutAware;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisSessionDAO;

import java.util.Collection;
import java.util.Objects;

/**
 * <p> Shiro工具类 </p>
 */
public class ShiroUtils {

    /**
     * 私有构造器
     **/
    private ShiroUtils() {
    }

    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

//    public static final String LOGIN_USER_IN_SESSION = "loginUser";

    /**
     * 传入一个用户放到session中
     * @param user
     */
//    public static void setUserInfo(User user){
//        Session session = getSession();
//        // 将用户信息放到session中
//        session.setAttribute(LOGIN_USER_IN_SESSION, user);
//    }

    /**
     * 从session中获取一个用户的信息
     */
//    public static User getUser(){
//        Session session = getSession();
//        return (User) session.getAttribute(LOGIN_USER_IN_SESSION);
//    }

    // -------------------------------------------------------------

    /**
     * 获取当前用户Session
     *
     * @Return SysUserEntity 用户信息
     */
    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }

    /**
     * 用户登出
     */
    public static void logout() {
        SecurityUtils.getSubject().logout();
    }

    /**
     * 获取当前用户信息
     *
     * @Return SysUserEntity 用户信息
     */
    public static UserDTO getUserInfo() {
        return (UserDTO) SecurityUtils.getSubject().getPrincipal();
    }

    /**
     * 获取当前登陆用户id
     *
     * @Return
     */
    public static String getLoginUserId() {
        UserDTO userDTO = (UserDTO) SecurityUtils.getSubject().getPrincipal();
        if (userDTO == null) {
            throw new ProjectExeption(ExceptionEnum.UNLOGIN);
        }
        return userDTO.getId();
    }

    /**
     * 删除用户缓存信息
     *
     * @Param username  用户名称
     * @Param isRemoveSession 是否删除Session,删除后用户需重新登录
     */
    public static void deleteCache(String username, boolean isRemoveSession) {
        //从缓存中获取Session
        Session session = null;
        // 获取当前已登录的用户session列表
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        UserDTO sysUserEntity;
        Object attribute = null;
        // 遍历Session,找到该用户名称对应的Session
        for (Session sessionInfo : sessions) {
            attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }
            sysUserEntity = (UserDTO) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if (sysUserEntity == null) {
                continue;
            }
            if (Objects.equals(sysUserEntity.getLoginName(), username)) {
                session = sessionInfo;
                // 清除该用户以前登录时保存的session,强制退出  -> 单用户登录处理
                if (isRemoveSession) {
                    redisSessionDAO.delete(session);
                }
            }
        }

        if (session == null || attribute == null) {
            return;
        }
        //删除session
        if (isRemoveSession) {
            redisSessionDAO.delete(session);
        }
        //删除Cache,再访问受限接口时会重新授权
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        Authenticator authc = securityManager.getAuthenticator();
        ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
    }

    /**
     * 从缓存中获取指定用户名的Session
     *
     * @param username
     */
    private static Session getSessionByUsername(String username) {
        // 获取当前已登录的用户session列表
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        SysUser user;
        Object attribute;
        // 遍历Session,找到该用户名称对应的Session
        for (Session session : sessions) {
            attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }
            user = (SysUser) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if (user == null) {
                continue;
            }
            if (Objects.equals(user.getLoginName(), username)) {
                return session;
            }
        }
        return null;
    }

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

意田天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值