• SpringBoot【六】 Shiro


    Shiro

    • Apache Shiro 是一个 java 的安全(权限)框架
    • 可以容易的开发出足够好的应用,不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境
    • 可以完成认证、授权、加密、会话管理、Web 集成、缓存等

    官网:https://shiro.apache.org/

    功能

    https://shiro.apache.org/introduction.html

    img

    Shiro 架构

    外部

    https://shiro.apache.org/architecture.html

    从外部来看 Shiro,即从应用程序角度来观察如何使用 Shrio 完成工作

    img

    • Subject 用户
    • SecurityManager 管理所有用户
    • Realm 连接数据

    内部

    img

    快速入门

    参考:https://github.com/apache/shiro/tree/master/samples/quickstart

    1. 导入依赖

      <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
      <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-core</artifactId>
          <version>1.5.3</version>
      </dependency>
      
      <!-- configure logging -->
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>1.7.21</version>
      </dependency>
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.7.21</version>
      </dependency>
      <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
      </dependency>
      
    2. 配置文件

      log4j.properties

      log4j.rootLogger=INFO, stdout
      
      log4j.appender.stdout=org.apache.log4j.ConsoleAppender
      log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
      log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
      
      # General Apache libraries
      log4j.logger.org.apache=WARN
      
      # Spring
      log4j.logger.org.springframework=WARN
      
      # Default Shiro logging
      log4j.logger.org.apache.shiro=INFO
      
      # Disable verbose logging
      log4j.logger.org.apache.shiro.util.ThreadContext=WARN
      log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
      

      shiro.ini(IDEA 中要先加入 Ini 插件)

      [users]
      # user 'root' with password 'secret' and the 'admin' role
      root = secret, admin
      # user 'guest' with the password 'guest' and the 'guest' role
      guest = guest, guest
      # user 'presidentskroob' with password '12345' ("That's the same combination on
      # my luggage!!!" ;)), and role 'president'
      presidentskroob = 12345, president
      # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
      darkhelmet = ludicrousspeed, darklord, schwartz
      # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
      lonestarr = vespa, goodguy, schwartz
      
      # -----------------------------------------------------------------------------
      # Roles with assigned permissions
      #
      # Each line conforms to the format defined in the
      # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
      # -----------------------------------------------------------------------------
      [roles]
      # 'admin' role has all permissions, indicated by the wildcard '*'
      admin = *
      # The 'schwartz' role can do anything (*) with any lightsaber:
      schwartz = lightsaber:*
      # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
      # license plate 'eagle5' (instance specific id)
      goodguy = winnebago:drive:eagle5
      
    3. QuickStart.class 分析

      一些常用方法:

      // 获取当前用户对象
      Subject currentUser = SecurityUtils.getSubject();
      // 根据当前用户拿到 session
      Session session = currentUser.getSession();
      // 判断当前用户是否被认证
      currentUser.isAuthenticated()    
      currentUser.getPrincipal()    
      currentUser.hasRole("schwartz")
      currentUser.isPermitted("lightsaber:wield")
      // 注销    
      currentUser.logout();
      

    SpringBoot 集成 Shiro

    环境搭建

    1. 新建 SpringBoot 项目,添加 web、thymeleaf 依赖

    2. 编写 Controller

      @Controller
      public class MyController {
      
          @RequestMapping({"/","/index"})
          public String toIndex(Model model){
              model.addAttribute("msg", "hello");
              return "index";
          }
      }
      
    3. 前端页面

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      
      <h1>首页</h1>
      
      <p th:text="${msg}"></p>
      
      </body>
      </html>
      
    4. 测试,环境 OK!

    Shiro 核心配置

    1. 导入 shrio 整合 Spring 的依赖

      <!--导入 shiro 整合 Spring -->
      <dependency>
         <groupId>org.apache.shiro</groupId>
         <artifactId>shiro-spring</artifactId>
         <version>1.5.3</version>
      </dependency>
      
    2. 编写 Shiro 的配置类 ShiroConfig.class,配置三个 Bean,即三大核心要素(对应三大核心对象):

      • ShiroFilterFactoryBean:过滤对象【第 3 步:连到前端】-------> Subject 用户
      • DefaultWebSecurityManager:安全对象【第 2 步:接管】--------> SecurityManager 管理所有用户
      • Realm 对象,需要自定义【第 1 步:创建对象】-------> Realm 连接数据
      @Configuration
      public class ShiroConfig {
          // ShiroFilterFactoryBean:3
          
          // DefaultWebSecurityManager:2
       
          // 创建 realm 对象,需要自定义:1
      }  
      
    3. 自定义 Realm 对象,需要继承 AuthorizingRealm,重写两个方法:认证和授权

      // 自定义的realm  extends AuthorizingRealm
      public class UserRealm extends AuthorizingRealm{
      
          // 授权
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
              System.out.println("执行了 授权 doGetAuthorizationInfo");
              return null;
          }
      
          // 认证
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
              System.out.println("执行了 认证 doGetAuthenticationInfo");   
              return null;
          }
      }
      
    4. 将自定义的 Realm 对象注入 Bean 中

      // 创建 realm 对象,需要自定义:1
      @Bean
      public UserRealm userRealm(){
          return new UserRealm();
      }
      
    5. 创建 DefaultWebSecurityManager 对象并注入 Bean 中,需要关联 Realm 对象(通过传参实现),因为它要对 Realm 对象进行管理

      // DefaultWebSecurityManager:2
      @Bean(name = "securityManager")
      public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
          DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
          // 关联 userRealm
          securityManager.setRealm(userRealm);
          return  securityManager;
      }
      
    6. 创建 ShiroFilterFactoryBean 对象并注入 Bean 中,需要关联 securityManager(通过传参)

      // ShiroFilterFactoryBean:3
      @Bean
      public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
          ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
          // 设置安全管理器
          bean.setSecurityManager(defaultWebSecurityManager);
          return bean;
      }
      

    登录拦截

    1. 编写两个前端页面 /user/add 和 /user/update

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      
      <h1>add</h1>
      
      </body>
      </html>
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      
      <h1>update</h1>
      
      </body>
      </html>
      
    2. Controller 跳转

      @RequestMapping("/user/add")
      public String add(){
          return "user/add";
      }
      
      @RequestMapping("/user/update")
      public String update(){
          return "user/update";
      }
      
    3. 首页加入两个跳转的超链接

      <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
      
    4. 测试,点击首页的 add 和 update,两个页面都可以进去

    5. 增加需求:对于某个页面,有些用户可以访问,有些用户不可以访问

    6. 在 ShiroFilterFactoryBean 对象,添加 Shiro 的内置过滤器

      anon:无需认证就可以访问
      authc:必须认证才可以访问
      user:必须拥有 记住我 功能才能用
      perms: 拥有对某个资源的权限才能访问
      role:拥有某个角色的权限才能访问
      
      Map<String, String> filterMap = new LinkedHashMap<>();
      // 登录拦截
      filterMap.put("/user/add", "authc");
      filterMap.put("/user/update", "authc");
      // filterMap.put("/user/*", "authc"); // 总和上面两个的作用,*为通配符
      
      bean.setFilterChainDefinitionMap(filterMap);
      
      Shiro 的内置过滤器源码:
      运行 Web 应用时,Shiro会创建一些有用的默认 Filter 实例,并自动地在 [main] 项中将它们置为可用,
      这些可用的默认的 Filter 实例是被 DefaultFilter 枚举类定义的(枚举的名称字段就是可供配置的名称)
      public enum DefaultFilter {
          anon(AnonymousFilter.class),
          authc(FormAuthenticationFilter.class),
          authcBasic(BasicHttpAuthenticationFilter.class),
          authcBearer(BearerHttpAuthenticationFilter.class),
          logout(LogoutFilter.class),
          noSessionCreation(NoSessionCreationFilter.class),
          perms(PermissionsAuthorizationFilter.class),
          port(PortFilter.class),
          rest(HttpMethodPermissionFilter.class),
          roles(RolesAuthorizationFilter.class),
          ssl(SslFilter.class),
          user(UserFilter.class);
      }
      
    7. 测试,点击 add 和 update 都会跳转到错误页码,证明拦截成功

    8. 想要拦截之后跳转到登录页面,需要先编写一个登录页面

      <h1>登录</h1>
      
      <p th:text="${msg}" style="color:red;"></p>
      <form th:action="@{/login}">
          <p>用户名:<input type="text" name="username"></p>
          <p>密码:<input type="text" name="password"></p>
          <p><input type="submit"></p>
      </form>
      
    9. 对应的需要在 Controller 中进行跳转

      @RequestMapping("/toLogin")
      public String toLogin(){
          return "login";
      }
      
    10. 在 ShiroFilterFactoryBean 中配置,如果没有权限,让其跳转到登录页面

      // 设置登录的请求
      bean.setLoginUrl("/toLogin");
      
    11. 测试成功,点击 add 和 update 都会被拦截,而且跳转到自己编写的登录页面

    登录拦截的 ShiroFilterFactoryBean 中配置总结

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
    	// 登录拦截
        Map<String, String> filterMap = new LinkedHashMap<>();
        // 添加 shiro 的内置过滤器
        filterMap.put("/user/*", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
        // 设置登录的请求
        bean.setLoginUrl("/toLogin");
    
        return bean;
    }
    

    用户认证

    用户的认证和授权在 Realm 对象中进行设置,然后和其他两个核心对象进行联动。

    1. 在 Controller 中通过前端提交的表单数据,获取当前用户信息并封装为令牌,对令牌执行登录的方法,如果信息错误则抛出异常(这些异常是 Shiro 已经定义好的)

      @RequestMapping("/login")
      public String login(String username, String password, Model model){
          // 获取当前的用户
          Subject subject = SecurityUtils.getSubject();
          // 封装用户的登录数据
          UsernamePasswordToken token = new UsernamePasswordToken(username, password);
      
          try{
              subject.login(token); // 执行登录的方法,如果没有异常就说明 OK 了
      
              return "index"; // 登录成功,返回首页
          } catch (UnknownAccountException e){ //用户名不存在
              model.addAttribute("msg", "用户名错误");
              return "login";
          } catch (IncorrectCredentialsException ice){ //密码不存在
              model.addAttribute("msg", "密码错误");
              return "login";
          }
      }
      
    2. 在登录页面的表单上方添加一个提示信息,如果用户名或密码错误会提示

      <p th:text="${msg}" style="color:red;"></p>
      
    3. 测试,在表单中填写信息,会提示错误信息,因为我们还没有认证用户名和密码

      注意:IDEA 控制台会显示 “执行了 认证 doGetAuthenticationInfo”,说明执行了 UserRealm 的认证方法!

      源码分析:在使用 SecurityUtils 的静态方法返回 getSubject() 之前,静态变量 securityManager 已经被加载,因为 securityManager 中管理着 Realm 对象,所以会执行 Realm 中的方法,但是为什么是执行了认证方法呢?是因为执行了 login() 方法,会将 token 传到 Authentication 吧

      // 源码:
      public abstract class SecurityUtils {
          private static SecurityManager securityManager;
      
          public static Subject getSubject() {
             ...
              return subject;
          }
      }
      
      /* @param token 
       * the token encapsulating the subject's principals and credentials to be passed to the Authentication subsystem for verification.
       */
      void login(AuthenticationToken token) throws AuthenticationException;
      
    4. 因为执行了认证方法,所以我们可以在该方法中做一些操作:取出数据库中真实的用户信息,用于和用户填写的信息进行比对

      // 认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
          System.out.println("执行了 认证 doGetAuthenticationInfo");
      
          // 用户名,密码,数据库中取
          String name = "root";
          String password = "123456";
      
          UsernamePasswordToken userToken = (UsernamePasswordToken) token;
      
          if (!userToken.getUsername().equals(name)){
              return null; //抛出异常 UnknownAccountException
          }
      
          // 可以加密 MD5 、MD5盐值加密
          // 密码认证 shiro 做,加密了
          return new SimpleAuthenticationInfo("",password,"");
      }
      
    5. 测试,用户名和密码分别填写 root 和 123456,登录成功可以访问 add 和 update 页面。

    用户认证:连接数据库,整合 MyBatis
    1. 导入依赖,使用 Druid 数据源

      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
      </dependency>
      <!-- Druid -->
      <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>druid</artifactId>
         <version>1.1.21</version>
      </dependency>
      <dependency>
         <groupId>log4j</groupId>
         <artifactId>log4j</artifactId>
         <version>1.2.17</version>
      </dependency>
      
      <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <version>1.18.10</version>
      </dependency>
      
      <!-- mybatis-spring-boot-starter:整合 -->
      <dependency>
         <groupId>org.mybatis.spring.boot</groupId>
         <artifactId>mybatis-spring-boot-starter</artifactId>
         <version>2.1.1</version>
      </dependency>
      
    2. Druid 数据源信息 application.yaml

      spring:
        datasource:
          username: root
          password: root
          url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
          driver-class-name: com.mysql.jdbc.Driver
          # 自定义数据源
          type: com.alibaba.druid.pool.DruidDataSource
      
          #Spring Boot 默认是不注入这些属性值的,需要自己绑定
          #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:监控统计、log4j:日志记录、wall:防御sql注入
          #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
          #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
          filters: stat,wall,log4j
          maxPoolPreparedStatementPerConnectionSize: 20
          useGlobalDataSourceStat: true
          connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      
    3. 将数据库绑定到 IDEA 中

    4. 配置 MyBatis,application.properties

      mybatis.type-aliases-package=com.song.pojo
      mybatis.mapper-locations=classpath:mapper/*.xml
      
    5. 根据数据库表的信息编写实体类,提前导入 Lombok

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
          private int id;
          private String name;
          private String pwd;
          private String perms;
      }
      
    6. 编写 mapper 接口 UserMapper.class

      @Repository
      @Mapper
      public interface UserMapper {
      
          public User queryUserByName(String name);
      }
      
    7. 编写对应的 mapper 配置文件 UserMapper.xml,放在 resources/mapper 文件夹下

      <?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.song.mapper.UserMapper">
      
          <select id="queryUserByName" resultType="User" parameterType="String">
              select * from User where name = #{name}
          </select>  
      </mapper>
      
    8. 编写 service 层,UserService 接口及实现类

      public interface UserService {
          public User queryUserByName(String name);
      }
      
      @Service
      public class UserServiceImpl implements UserService{
      
          @Autowired
          UserMapper userMapper;
          
          @Override
          public User queryUserByName(String name) {
              return userMapper.queryUserByName(name);
          }
      }
      
    9. 测试,成功输出数据库中的“张三”对象,说明前面编写的代码没有问题

      @SpringBootTest
      class ShiroSpringbootApplicationTests {
         @Autowired
         UserService userService;
          
         @Test
         void contextLoads() {
            System.out.println(userService.queryUserByName("张三"));
         }
      }
      
    10. 用户认证:Realm 的认证方法中使用连接的数据库中的真实的数据

      @Autowired
      UserService userService;
      
       // 认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
          System.out.println("执行了 认证 doGetAuthenticationInfo");
      
          UsernamePasswordToken userToken = (UsernamePasswordToken) token;
          // 连接真实数据库
          User user = userService.queryUserByName(userToken.getUsername());
          if (user == null){ // 没有这个人
              return null; //UnknownAccountException
          }
          // 密码可以加密: MD5 、MD5盐值加密
          return new SimpleAuthenticationInfo("",user.getPwd(),"");
      }
      
    11. 测试,成功

    用户授权

    1. 在 ShiroFilterFactoryBean 设置权限,并设置跳转的未授权页面,当进入设置权限的页面 /user/add 和 /user/update 时,会自动执行 Rleam 中的授权方法,所以要在 Rleam 的授权方法中做具体的授权操作

      // ShiroFilterFactoryBean:3
          @Bean
          public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
              ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
              // 设置安全管理器
              bean.setSecurityManager(defaultWebSecurityManager);
              Map<String, String> filterMap = new LinkedHashMap<>();
      
              // 通过 shiro 的内置过滤器
              // 授权,正常情况下没有授权会跳转到未授权页面
              filterMap.put("/user/add","perms[user:add]");
              filterMap.put("/user/update","perms[user:update]");
      
              // 登录拦截
              filterMap.put("/user/*", "authc");
              bean.setFilterChainDefinitionMap(filterMap);
              // 设置登录的请求
              bean.setLoginUrl("/toLogin");
              
              // 未授权页
              bean.setUnauthorizedUrl("/noauth");
      
              return bean;
          }
      
    2. Controller 跳转到未授权页面

      @RequestMapping("/noauth")
      @ResponseBody
      public String unauthrized(){
          return "未经授权无法访问此页面";
      }
      
    3. 测试,点击 add 会跳转到未授权页面,并且所有用户都是未授权,接下来要给用户授予访问的权限!

    4. ShiroFilterFactoryBean 只是设置了权限,但是怎么把这个权限赋给用户呢?真正的授权操作在 Rleam 的授权方法中

      // 授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
          System.out.println("执行了 授权 doGetAuthorizationInfo");
      
          // SimpleAuthorizationInfo
          SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
      	// 授权
      	info.addStringPermission("user:add");
      
          return info;
      }
      
    5. 测试,/user/add 页面每个用户都可以进去,因为每个用户进入授权方法后都被授予了访问的权限,而在实际中不应该这样硬编码授权操作,应该根据数据库中的权限信息进行授权操作!

    6. 为数据库中的 User 表添加权限字段,对应实体类的属性字段也应该修改

    7. 根据数据库的 perms 字段设置权限

      // 授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
          System.out.println("执行了 授权 doGetAuthorizationInfo");
      
          // SimpleAuthorizationInfo
          SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
      //	info.addStringPermission("user:add");
      
          // 拿到当前登录的这个对象
          Subject subject = SecurityUtils.getSubject();
          // user <= return new SimpleAuthenticationInfo(user,user.getPwd(),""); 
          // 第一个参数user,getPrincipal() 取出的是认证方法返回的对象中存的这个user
          User currentUser = (User) subject.getPrincipal(); // 拿到User对象
          // 设置当前用户的权限
          info.addStringPermission(currentUser.getPerms());
      
          return info;
      }
      
      // SimpleAuthenticationInfo 源码
      /* @param principal   the 'primary' principal associated with the specified realm.
       * @param credentials the credentials that verify the given principal.
       * @param realmName   the realm from where the principal and credentials were acquired.
       */
      public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
          this.principals = new SimplePrincipalCollection(principal, realmName);
          this.credentials = credentials;
      }
      
    8. 测试,用户“root”有访问 update 页面的权限,用户“张三”有访问 add 页面的权限,其他用户都不能访问这两个页面。

    页面优化

    让用户登录之后显示的信息不一样,比如:用户“root”登录之后只显示 update,而用户“张三”登录只显示 add,其他用户什么都不显示。可以使用 thymeleaf 进行操作。

    1. 导入 thymeleaf 整合包(和 Spring Security 很相似 thymeleaf-extras-springsecurity4)

      <!--thymeleaf 和 shiro 整合包-->
      <dependency>
         <groupId>com.github.theborakompanioni</groupId>
         <artifactId>thymeleaf-extras-shiro</artifactId>
         <version>2.0.0</version>
      </dependency>
      
    2. 在 ShiroConfig 中装配 Bean 来整合 shiro 和 thymeleaf

      // 整合 ShiroDialect:用来整合 shiro thymeleaf
      @Bean
      public ShiroDialect getShiroDialect(){
          return new ShiroDialect();
      }
      
    3. 修改前端 index.html 页面

      先导入 shrio 的的命名空间

      xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
      

      修改之前的 add 和 update 页面

      <div shiro:hasPermission="user:add">
      <a th:href="@{/user/add}">add</a>
      </div>
      
      <div shiro:hasPermission="user:update">
      <a th:href="@{/user/update}">update</a>
      </div>
      
    4. 测试,需求实现,但是,当没有权限时 add 和 update 都没有,那么也就无法跳转到登录页面,说明缺少一个登录按钮,所以要增加一个登录按钮,让其在未登录时显示,登录之后不显示

    5. 编写前端代码,添加登录按钮

      <!--从 session 中判断值-->
      <!--<div shiro:guest="true">-->
      <div th:if="${session.loginUser==null}">
          <a th:href="@{/toLogin}" >登录</a>
      </div>
      
    6. 在认证时查出来用户之后将信息传到前端,用来控制登录按钮的显示效果【这一步操作可以不写,可以在前端直接用 shiro 标签 shiro:guest="true" 来实现】

      Subject currentSubject = SecurityUtils.getSubject();
      Session session = currentSubject.getSession();
      session.setAttribute("loginUser",user); //前端可以拿到这个 user
      
    7. 测试,登录按钮的显示效果成功实现

    总结

    1、Controller 层
    @Controller
    public class MyController {
    
        @RequestMapping({"/","/index"})
        public String toIndex(Model model){
            model.addAttribute("msg", "hello");
            return "index";
        }
    
        @RequestMapping("/user/add")
        public String add(){
            return "user/add";
        }
    
        @RequestMapping("/user/update")
        public String update(){
            return "user/update";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin(){
            return "login";
        }
    
        @RequestMapping("/login")
        public String login(String username, String password, Model model){
            // 获取当前的用户
            Subject subject = SecurityUtils.getSubject();
            // 封装用户的登录数据
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    
            try{
                subject.login(token); // 执行登录的方法,如果没有异常就说明 OK 了
    
                return "index";
            } catch (UnknownAccountException e){ //用户名不存在
                model.addAttribute("msg", "用户名错误");
                return "login";
            } catch (IncorrectCredentialsException ice){ //密码不存在
                model.addAttribute("msg", "密码错误");
                return "login";
            }
        }
    
        @RequestMapping("/noauth")
        @ResponseBody
        public String unauthrized(){
            return "未经授权无法访问此页面";
        }
    }
    
    2、自定义的 Realm
    // 自定义的realm  extends AuthorizingRealm
    public class UserRealm extends AuthorizingRealm{
    
        @Autowired
        UserService userService;
    
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("执行了 授权 doGetAuthorizationInfo");
    
            // SimpleAuthorizationInfo
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    		// 每个用户都会被授予 user:add 权限
    //        info.addStringPermission("user:add");
    
            // 拿到当前登录的这个对象
            Subject subject = SecurityUtils.getSubject();
            // user <= return new SimpleAuthenticationInfo(user,user.getPwd(),""); 
            // 第一个参数user,getPrincipal() 取出的就是认证方法返回的对象中存的这个user
            User currentUser = (User) subject.getPrincipal();
            // 设置当前用户的权限
            info.addStringPermission(currentUser.getPerms());
            
            return info;
        }
    
        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("执行了 认证 doGetAuthenticationInfo");
    
           /* // 用户名,密码,数据库中取,这里未连接数据可
            String name = "root";
            String password = "123456";
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            if (!userToken.getUsername().equals(name)){
                return null; //抛出异常 UnknownAccountException
            }
            // 密码认证 shiro 做,加密了
            return new SimpleAuthenticationInfo("",password,"");*/
    
    		// 连接真实数据库
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;       
            User user = userService.queryUserByName(userToken.getUsername());
            if (user == null){ // 没有这个人
                return null; //UnknownAccountException
            }
    
    		// 用于登录按钮是否显示
            Subject currentSubject = SecurityUtils.getSubject();
            Session session = currentSubject.getSession();
            session.setAttribute("loginUser",user); //前端可以拿到这个 user
    
    //        return new SimpleAuthenticationInfo("",user.getPwd(),"");
            return new SimpleAuthenticationInfo(user,user.getPwd(),"");//第一个参数用于授权
        }
    }
    
    3、ShrioConfig 配置类
    @Configuration
    public class ShiroConfig {
    
        // ShiroFilterFactoryBean:3
        @Bean
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            // 设置安全管理器
            bean.setSecurityManager(defaultWebSecurityManager);
    
            // 添加 shiro 的内置过滤器
            Map<String, String> filterMap = new LinkedHashMap<>();
    
            // 授权,正常情况下没有授权会跳转到未授权页面
            filterMap.put("/user/add","perms[user:add]");
            filterMap.put("/user/update","perms[user:update]");
    
            // 登录拦截
    //        filterMap.put("/user/add", "authc");
    //        filterMap.put("/user/update", "authc");
            filterMap.put("/user/*", "authc");
    
            bean.setFilterChainDefinitionMap(filterMap);
    
            // 设置登录的请求
            bean.setLoginUrl("/toLogin");
            // 未授权页面
            bean.setUnauthorizedUrl("/noauth");
            return bean;
        }
    
        // DefaultWebSecurityManager:2
        @Bean(name = "securityManager")
        public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 关联 userRealm
            securityManager.setRealm(userRealm);
            return  securityManager;
        }
    
        // 创建 realm 对象,需要自定义:1
        @Bean
        public UserRealm userRealm(){
            return new UserRealm();
        }
    
        // 整合 ShiroDialect:用来整合 shiro thymeleaf
        @Bean
        public ShiroDialect getShiroDialect(){
            return new ShiroDialect();
        }
    }
    
    4、前端页面 index.html 和 login.html
    <!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</title>
    </head>
    <body>
    
        <h1>首页</h1>
        <!--从 session 中判断值-->
        <!--<div shiro:guest="true">-->
        <div th:if="${session.loginUser==null}">
            <a th:href="@{/toLogin}" >登录</a>
        </div>
    
        <p th:text="${msg}"></p>
        <hr>
    
        <div shiro:hasPermission="user:add">
            <a th:href="@{/user/add}">add</a>
        </div>
    
        <div shiro:hasPermission="user:update">
            <a th:href="@{/user/update}">update</a>
        </div>
    
    </body>
    </html>
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <h1>登录</h1>
    
    <p th:text="${msg}" style="color:red;"></p>
    <form th:action="@{/login}">
        <p>用户名:<input type="text" name="username"></p>
        <p>密码:<input type="text" name="password"></p>
        <p><input type="submit"></p>
    </form>
    
    </body>
    </html>
    
  • 相关阅读:
    framwork NHibernate
    java eclise的配置
    java jdk环境变量配置
    第零章 关于课程教材与讲义
    ActiveMQ(5.10.0)
    ActiveMQ(5.10.0)
    ActiveMQ(5.10.0)
    ActiveMQ(5.10.0)
    ActiveMQ(5.10.0)
    ActiveMQ(5.10.0)
  • 原文地址:https://www.cnblogs.com/Songzw/p/13295348.html
Copyright © 2020-2023  润新知