Shiro在SpringBoot中的应用

本文详细介绍了Apache Shiro权限管理框架的使用,包括数据库表设计、pom依赖配置、自定义Realm、Shiro配置、过滤器应用、授权、异常处理、会话管理以及结合Redis的统一会话管理等关键步骤,通过SpringBoot项目演示了如何实现安全控制。

Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。越来越多的企业使用Shiro作为项目的安全框架,保证项目的平稳运行。

1.数据库表

 

2.pom依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--shiro和spring整合-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--shiro核心包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--shiro与redis整合-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>

3.自定义Realm

Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源

public class CustomRealm extends AuthorizingRealm {
​
    @Override
    public void setName(String name) {
        super.setName("customRealm");
    }
​
    @Autowired
    private UserService userService;
​
    /**
     * 授权方法
     * 操作的时候,判断用户是否具有响应的权限
     * 先认证 -- 安全数据
     * 再授权 -- 根据安全数据获取用户具有的所有操作权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取已认证的用户数据
        User user = (User) principalCollection.getPrimaryPrincipal();//得到唯一的安全数据
        //2.根据用户数据获取用户的权限信息(所有角色,所有权限)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet<>();//所有角色
        Set<String> perms = new HashSet<>();//所有权限
        for (Role role : user.getRoles()) {
            roles.add(role.getName());
            for (Permission perm : role.getPermissions()) {
                perms.add(perm.getCode());
            }
        }
        info.setStringPermissions(perms);
        info.setRoles(roles);
        return info;
    }
​
​
    /**
     * 认证方法
     * 参数:传递的用户名密码
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取登录的用户名密码(token)
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());
        //2.根据用户名查询数据库
        User user = userService.findByName(username);
        //3.判断用户是否存在或者密码是否一致
        if (user != null && user.getPassword().equals(password)) {
            //4.如果一致返回安全数据
            //构造方法:安全数据,密码,realm域名
            return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        }
        //5.不一致,返回null(抛出异常)
        return null;
    }
​
}

4.Shiro的配置

SecurityManager 是 Shiro 架构的心脏,用于协调内部的多个组件完成全部认证授权的过程。例如通过调用realm完成认证与登录。使用基于springboot的配置方式完成SecurityManager,Realm的装配。

@Configuration
public class ShiroConfiguration {
​
    //1.创建realm
    @Bean
    public CustomRealm getRealm() {
        return new CustomRealm();
    }
​
    //2.创建安全管理器
    @Bean
    public SecurityManager getSecurityManager(CustomRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
​
        //将自定义的会话管理器注册到安全管理器中
        securityManager.setSessionManager(sessionManager());
        //将自定义的redis缓存管理器注册到安全管理器中
        securityManager.setCacheManager(cacheManager());
​
        return securityManager;
    }
​
    //3.配置shiro的过滤器工厂
    /**
     * 在web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        //1.创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        //2.设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        //3.通用配置(跳转登录页面,为授权跳转的页面)
        filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
        filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url
        //4.设置过滤器集合
​
        /**
         * 设置所有的过滤器:有顺序map
         *     key = 拦截的url地址
         *     value = 过滤器类型
         *
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        //filterMap.put("/user/home","anon");//当前请求地址可以匿名访问
​
        //具有某中权限才能访问
        //使用过滤器的形式配置请求地址的依赖权限
        //filterMap.put("/user/home","perms[user-home]"); //不具备指定的权限,跳转到setUnauthorizedUrl地址
​
        //使用过滤器的形式配置请求地址的依赖角色
        //filterMap.put("/user/home","roles[系统管理员]");
​
        filterMap.put("/user/**", "authc");//当前请求地址必须认证之后可以访问
​
        filterFactory.setFilterChainDefinitionMap(filterMap);
​
        return filterFactory;
    }
​
    /**
     * 开启对shiro注解的支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

5.Shiro中的过滤器

 

注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到shiroFilterFactoryBean.setLoginUrl()设置的 url )

6.授权

(1)基于配置的授权

//配置请求连接过滤器配置
//匿名访问(所有人员可以使用) 
filterMap.put("/user/home", "anon"); 
//具有指定权限访问
filterMap.put("/user/find", "perms[user-find]"); 
//认证之后访问(登录之后可以访问) 
filterMap.put("/user/**", "authc"); 
//具有指定角色可以访问
filterMap.put("/user/**", "roles[系统管理员]");

(2)基于注解的授权

配置到方法上,表明执行此方法必须具有指定的权限

//查询
@RequiresPermissions(value = "user-find") public String find() {
return "查询用户成功"; }

配置到方法上,表明执行此方法必须具有指定的角色

//查询
@RequiresRoles(value = "系统管理员") public String find() {
return "查询用户成功"; }

7.统一异常处理

基于注解的配置方式进行授权,一旦操作用户不具备操作权限,目标方法不会被执行,而且会抛出AuthorizationException 异常。所以需要做好统一异常处理完成未授权处理

/**
 * 自定义的公共异常处理器
 *      1.声明异常处理器
 *      2.对异常统一处理
 */
@ControllerAdvice
public class BaseExceptionHandler {
​
    @ExceptionHandler(value = AuthorizationException.class)
    @ResponseBody
    public String error(HttpServletRequest request, HttpServletResponse response,AuthorizationException e) {
        return "未授权";
    }
}

8.会话管理

在shiro里所有的用户的会话信息都会由Shiro来进行控制,shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过Shiro的会话管理器(SessionManager)进行统一的会话管理。

(1)什么是shiro的会话管理

SessionManager(会话管理器):管理所有Subject的session包括创建、维护、删除、失效、验证等工作。

SessionManager是顶层组件,由SecurityManager管理 shiro提供了三个默认实现:

  1. DefaultSessionManager:用于JavaSE环境

  2. ServletContainerSessionManager:用于Web环境,直接使用servlet容器的会话。

  3. DefaultWebSessionManager:用于web环境,自己维护会话(自己维护着会话,直接废弃了Servlet容器的会话管理)。

(2)应用场景

在分布式系统或者微服务架构下,都是通过统一的认证中心进行用户认证。如果使用默认会话管理,用户信息只会保存到一台服务器上。那么其他服务就需要进行会话的同步。

 

(3)Shiro结合redis的统一会话管理

自定义shiro会话管理器

/**
 * 自定义的sessionManager
 */
public class CustomSessionManager extends DefaultWebSessionManager {
​
​
    /**
     * 头信息中具有sessionid
     *      请求头:Authorization: sessionid
     *
     * 指定sessionId的获取方式
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
​
        //获取请求头Authorization中的数据
        String id = WebUtils.toHttp(request).getHeader("Authorization");
        if(StringUtils.isEmpty(id)) {
            //如果没有携带,生成新的sessionId
            return super.getSessionId(request,response);
        }else{
            //返回sessionId;
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }
    }
}

配置Shiro基于redis的会话管理

@Configuration
public class ShiroConfiguration {  
    
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
​
    /**
     * 1.redis的控制器,操作redis
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        return redisManager;
    }
​
    /**
     * 2.sessionDao
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        return sessionDAO;
    }
​
    /**
     * 3.会话管理器
     */
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }
​
    /**
     * 4.缓存管理器
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
}

9.测试方法

整体架构为SpringBoot+SpringMVC+Spring Data Jpa

service,dao,entity已省略,controller层代码如下:

@RestController
public class UserController {
​
    @Autowired
    private UserService userService;
​
    //个人主页
    //使用shiro注解鉴权
    //@RequiresPermissions()  -- 访问此方法必须具备的权限
    //@RequiresRoles() -- 访问此方法必须具备的角色
​
    /**
     * 1.过滤器:如果权限信息不匹配setUnauthorizedUrl地址
     * 2.注解:如果权限信息不匹配,抛出异常
     */
    @RequiresPermissions("user-home")
    @RequestMapping(value = "/user/home")
    public String home() {
        return "访问个人主页成功";
    }
​
    //添加
    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String add() {
        return "添加用户成功";
    }
    
    //查询
    @RequestMapping(value = "/user",method = RequestMethod.GET)
    public String find() {
        return "查询用户成功";
    }
    
    //更新
    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    public String update(String id) {
        return "更新用户成功";
    }
    
    //删除
    @RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
    public String delete() {
        return "删除用户成功";
    }
​
    /**
     *  1.传统登录
     *      前端发送登录请求 => 接口部分获取用户名密码 => 程序员在接口部分手动控制
     *  2.shiro登录
     *      前端发送登录请求 => 接口部分获取用户名密码 => 通过subject.login =>  realm域的认证方法
     *
     */
    //用户登录
    @RequestMapping(value="/login")
    public String login(String username,String password) {
        //构造登录令牌
        try {
​
            /**
             * 密码加密:
             *     shiro提供的md5加密
             *     Md5Hash:
             *      参数一:加密的内容
             *              111111   --- abcd
             *      参数二:盐(加密的混淆字符串)(用户登录的用户名)
             *              111111+混淆字符串
             *      参数三:加密次数
             *
             */
            password = new Md5Hash(password,username,3).toString();
​
            UsernamePasswordToken upToken = new UsernamePasswordToken(username,password);
            //1.获取subject
            Subject subject = SecurityUtils.getSubject();
​
            //获取session
            String sid = (String) subject.getSession().getId();
​
            //2.调用subject进行登录
            subject.login(upToken);
            return "登录成功";
        }catch (Exception e) {
            return "用户名或密码错误";
        }
    }
}

配置文件如下:

server:
  port: 8081
spring:
  application:
    name: shiro-test #指定服务名
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro_db?useUnicode=true&characterEncoding=utf8
    username: root
    password: root
  jpa:
    database: MySQL
    show-sql: true
    open-in-view: true
  redis:
    host: 127.0.0.1
    port: 6379

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值