首页 > 编程语言 > 详细

SpringBoot【六】 Shiro

时间:2020-07-13 21:13:01      阅读:58      评论:0      收藏:0      [点我收藏+]

Shiro

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

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

功能

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

技术分享图片

Shiro 架构

外部

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

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

技术分享图片

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

内部

技术分享图片

快速入门

参考: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>

SpringBoot【六】 Shiro

原文:https://www.cnblogs.com/Songzw/p/13295348.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!