美文网首页Springboot 系列
Shiro:连接数据库实现认证、授权

Shiro:连接数据库实现认证、授权

作者: 掌灬纹 | 来源:发表于2021-01-08 19:29 被阅读0次

    Apache Shiro 一个简单的Java安全框架,在此框架基础上可以轻松实现网站的用户认证授权

    • Shiro的三大核心对象:
      1. Subject (代表当前用户对象)
      2. ShiroSecurityManager(管理所有Subject对象)
      3. Realm(管理数据:具体数据的授权与认证)

    一、用户认证与拦截器

    1.SpringBoot项目准备

    • 导入案例涉及依赖
        <dependencies>
            <!--mybatis druid数据原-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.12</version>
            </dependency>
    
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
    
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.10</version>
            </dependency>
    
    
            <!--shiro spring-->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.7.0</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <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>
    
            <!--整合thymeleaf-->
            <dependency>
                <groupId>com.github.theborakompanioni</groupId>
                <artifactId>thymeleaf-extras-shiro</artifactId>
                <version>2.0.0</version>
            </dependency>
        </dependencies>
    
    • 配置Druid数据源,连接数据库
    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        driver-class-name: com.mysql.jdbc.Driver
    
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
    • 配置Mybatis别名,mapper位置
    mybatis:
      type-aliases-package: com.ht.springbootshiro01.pojo
      mapper-locations: classpath:mapper/*.xml
    # 资源位置resources/mapper/*.xml
    
    • 编写Dao、Service层 测试Mybatis正常使用


      数据库表结构.jpg
    (1)Dao(Mapper)层逻辑代码(省略实体类)
    @Repository
    @Mapper
    // Dao 层
    public interface UserMapper {
    
        /**
         * 根据用户名 查询指定用户
         * @param name
         * @return
         */
        public User queryUserByName(@Param("name") String name);
    }
    
    (2)Mapper.xml文件编写(查询数据库)
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.ht.springbootshiro01.mapper.UserMapper">
    
        <select id="queryUserByName" parameterType="String" resultType="User">
            select * from mybatis.user where name=#{name};
        </select>
    
    </mapper>
    
    (3)Service层业务逻辑(调用Dao层)
    @Service
    public class UserService {
        // service 调用 dao层
        @Autowired
        UserMapper userMapper;
    
        public User queryUserByName(String name){
            return userMapper.queryUserByName(name);
        }
    }
    
    (4)测试代码(查询用户结果)
    @SpringBootTest
    class SpringbootShiro01ApplicationTests {
    
        @Autowired
        UserService userService;
    
        @Test
        void contextLoads() {
            // mybatis 测试
            User admin = userService.queryUserByName("admin");
            System.out.println(admin.toString());
        }
    }
    

    至此查询成功后,SpringBoot 整合MyBatis数据库准备工作完成

    2.基础网页路由配置

    • 首页,登录页,管理员权限下的显示所有用户网页


      页面分布.jpg
    • 跳转路由代码(Controller)
    /**
     * 管理路由跳转的Controller
     */
    @Controller
    public class RouteController {
    
        @RequestMapping({"/index","/"})
        public String index(Model model){
            model.addAttribute("msg","Hello Shiro");
            return "index";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin(){
            return "login";
        }
    
        //user
        @RequestMapping("/user/add")
        public String add(){
            return "user/add";
        }
    
        @RequestMapping("/user/update")
        public String update(){
            return "user/update";
        }
    
        // show
        @RequestMapping("/show")
        public String show(){
            return "show";
        }
    
    }
    

    3.编写Shiro配置文件,自定义Realm(管理授权、认证)

    • Shiro配置文件 配置三大要素:自定义Realm规则,SecurityManager安全管理对象,Shiro Filter拦截器相关信息配置

    • shiro的内置过滤器:

         * anon: 无需认证就可访问
         * authc: 认证后才可访问
         * user: 必须拥有记住我功能才可访问
         * perms:拥有对某个资源的权限才可访问
         * role: 拥有某个角色的权限才可访问
      
    @Configuration
    public class ShiroConfig {
    
        //1.创建Realm对象 自定义 注入bean
        @Bean(name = "getUserRealm")
        public UserRealm getUserRealm(){
            return new UserRealm();
        }
    
        //2.SecurityManager Spring 注入参数 bean Qualifier(方法名)
        @Bean(name = "getSecurityManager")
        public DefaultWebSecurityManager defaultSecurityManager(@Qualifier("getUserRealm") UserRealm userRealm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 关联 Realm
            securityManager.setRealm(userRealm);
            return securityManager;
        }
    
        //3.Shiro Filter
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getSecurityManager") DefaultSecurityManager securityManager){
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            // 设置 安全管理
            bean.setSecurityManager(securityManager);
            //过滤器配置
            Map<String, String> filterMap = new LinkedHashMap<>();
    
            // /user 请求 拦截 认证后可访问
            filterMap.put("/user/**","authc");
    
            // 开放登录接口 首页
            filterMap.put("/toLogin","anon");
            filterMap.put("/index","anon");
            filterMap.put("/","anon");
    
            // 关闭其它路由 需授权访问
            //filterMap.put("/**","authc");
    
            bean.setFilterChainDefinitionMap(filterMap);
            //System.out.println("Shiro拦截器注入成功");
    
            // 自定义登录页面路径 实现登录拦截
            bean.setLoginUrl("/toLogin");
    
            return bean;
        }
    
    }
    

    至此实现了Shiro的初步登录拦截,发现user/下的路由都不能访问

    4.编写自定义Realm规则,认证 继承AuthorizingRealm

    ~ token令牌实际应用规则:获取前端登录传递的用户名、密码信息封装令牌,Shiro框架会自动经过自定义Realm 认证规则检验,与数据库中用户信息匹配比较

    // 自定义UserRealm
    public class UserRealm extends AuthorizingRealm {
    
        @Autowired
        UserService userService;
    
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            System.out.println("执行了 授权 ==》" + principals.getRealmNames().toString());
            return null;
        }
    
        // token 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("执行了 认证 ==》" + token.getPrincipal().toString());
    
            // 数据库取数据 进行认证
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            // 前端传递 用户名 查询 数据库
            User user = userService.queryUserByName(userToken.getUsername());
    
            if (null == user){
                // 前端传递 与 数据库查询用户名 不一致
                return null; // 抛出异常 未知用户名
            }
    
            // 默认密码不加密(不安全) 可以做 md5加密 或 md5盐值加密
            // 密码认证 shiro做
            return new SimpleAuthenticationInfo("",user.getPwd(),"");
        }
    }
    
    • 登录请求处理:获取前端表单传递数据,封装令牌==》执行登录操作==》处理登录异常
    @Controller
    public class LoginController {
    
        /**
         * 登录请求 获取表单传递数据 封装令牌 token
         * @param username
         * @param password
         * @param model
         * @return
         */
        @PostMapping("/login.do")
        public String login(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            Model model){
            System.out.println("登录信息:username: " + username + " ; password: " + password);
            // 获取当前用户
            Subject user = SecurityUtils.getSubject();
            // 封装令牌 用于 Realm验证
            UsernamePasswordToken token = new UsernamePasswordToken(username,password);
    
            // 执行登录
            try {
                user.login(token); // 调用自定义 UserRealm 认证方法
                return "index";
            } catch (UnknownAccountException uae) {
                // 用户不存在
                System.out.println("There is no user with username of " + token.getPrincipal());
                model.addAttribute("msg","用户名不存在");
                return "login";
            } catch (IncorrectCredentialsException ice) {
                // 密码错误
                System.out.println("Password for account " + token.getPrincipal() + " was incorrect!");
                model.addAttribute("msg","密码错误");
                return "login";
            } catch (LockedAccountException lae) {
                // 当前用户锁定
                System.out.println("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
                model.addAttribute("msg","用户锁定");
                return "login";
            } catch (AuthenticationException ae) {
                //unexpected condition?  error?
                System.out.println("其它错误");
                model.addAttribute("msg","其它错误");
                return "login";
            }
    
        }
    }
    

    ~ 测试Shiro的用户认证


    测试认证.jpg

    二、用户授权与前端整合

    1.FilterMap中添加指定路由权限

           Map<String, String> filterMap = new LinkedHashMap<>();
            // 管理员开放权限
            //filterMap.put("/user/**","perms[admin]");
            // 授权 只有 拥有 user:add的权限才可访问 失败401 未授权
            filterMap.put("/user/add","perms[user:add]");
            filterMap.put("/user/update","perms[user:update]");
    
            // /user 请求 拦截 认证后可访问
            filterMap.put("/user/**","authc");
    
            // 开放登录接口 首页
            filterMap.put("/toLogin","anon");
            filterMap.put("/index","anon");
            filterMap.put("/","anon");
            // 关闭其它路由 需授权访问
            //filterMap.put("/**","authc");
    
            bean.setFilterChainDefinitionMap(filterMap);
            //System.out.println("Shiro拦截器注入成功");
    
            // 自定义登录页面路径 实现登录拦截
            bean.setLoginUrl("/toLogin");
            // 设置未授权请求
            bean.setUnauthorizedUrl("/unauthorized");
    

    2.编写定制未授权跳转页面及路由控制器

    /***
     * 未授权控制器
     */
    @Controller
    public class UnauthorizedController {
        /**
         * 未授权页面
         * @return
         */
        @RequestMapping("/unauthorized")
        public String toUnauthorized(){
            return "error/unauthor";
        }
    }
    

    *自定义未授权页面 /error/unauthor.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>未授权</title>
    </head>
    <body>
    <div>
        <h3 style="color: darkslategrey">未授权,请
            <a th:href="@{/toLogin}">切换用户</a>,或进行授权操作</h3>
    </div>
    </body>
    </html>
    

    3.根据数据库中用户权限 为用户授权

    • 自定义Realm 认证 传递给授权 携带(Principal == 令牌负责人即登录用户)
    • 授权方法 取出负责人用户,根据数据库中用户权限授权
    // 自定义UserRealm
    public class UserRealm extends AuthorizingRealm {
    
        @Autowired
        UserService userService;
    
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            System.out.println("执行了 授权 ==》" + principals.getRealmNames().toString());
    
            // 增加授权 所有用户都赋予权限
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            Subject subject = SecurityUtils.getSubject();
            User principal = (User) subject.getPrincipal();
    
            // 通过数据库字段 增加 权限
            info.addStringPermission(principal.getPerms());
    
            return info;
        }
    
        // token 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("执行了 认证 ==》" + token.getPrincipal().toString());
    
            // 数据库取数据 进行认证
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            // 前端传递 用户名 查询 数据库
            User user = userService.queryUserByName(userToken.getUsername());
    
            if (null == user){
                // 前端传递 与 数据库查询用户名 不一致
                return null; // 抛出异常 未知用户名
            }
    
            // 密码认证 shiro做 登录成功后将用户信息 传递给授权 
            return new SimpleAuthenticationInfo(user,user.getPwd(),"");
        }
    }
    

    4.增加前端显示用户信息 与 注销功能

    • 首页
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
      <meta charset="UTF-8">
      <title>首页</title>
    </head>
    <body>
    <div>
      <h1>首页</h1>
        <!---->
      <p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
    
        <!--用户信息-->
        <p th:text="${user_name}"
           th:if="${not #strings.isEmpty(user_name)}"></p>
    </div>
    <div>
      <a th:href="@{/user/add}">增加</a>
      <a th:href="@{/user/update}">修改</a>
      <a th:href="@{/show}">展示</a>
      <a th:href="@{/loginout.do}">注销</a>
    </div>
    
    </body>
    </html>
    
    • 注销控制器
        @RequestMapping("/loginout.do")
        public String loginOut(){
            Subject subject = SecurityUtils.getSubject();
            // 注销
            subject.logout();
            return "login";
        }
    
    • 登录成功 封装用户名
       user.login(token); // 调用自定义 UserRealm 认证方法
       model.addAttribute("user_name",token.getUsername());
       return "index";
    

    三、拓展整合Thymeleaf引擎

    • 实现不同权限的用户显示不同操作
    • 实现未登录用户 显示 登录按钮;已登录显示注销按钮
      重要标签:
    <!--头引用-->
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
    xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
    shiro:hasPermission="xxx" 拥有某权限
    shiro:guest="true" 未登录
    shiro:authenticated="true" 已登陆
    
    • 完整首页代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
    xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
    <head>
      <meta charset="UTF-8">
      <title>首页</title>
    </head>
    <body>
    <div>
      <h1>首页</h1>
        <!---->
      <p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
    
        <!--用户信息-->
        <p th:text="${user_name}"
           th:if="${not #strings.isEmpty(user_name)}"></p>
    </div>
    <div>
      <!--整合 shiro 根据权限显示-->
      <div shiro:hasPermission="user:add">
        <a th:href="@{/user/add}">增加</a>
      </div>
      <div shiro:hasPermission="user:update">
        <a th:href="@{/user/update}">修改</a>
      </div>
      <a th:href="@{/show}">展示</a>
      <!--游客显示登录按钮 登录成功不显示-->
      <div shiro:guest="true">
        <a th:href="@{/toLogin}">登录</a>
      </div>
      <div shiro:authenticated>
        <a th:href="@{/loginout.do}">注销</a>
      </div>
    </div>
    
    </body>
    </html>
    

    ~体验到Shiro框架的便捷之处,告别过滤器

    相关文章

      网友评论

        本文标题:Shiro:连接数据库实现认证、授权

        本文链接:https://www.haomeiwen.com/subject/eivxaktx.html