M

MAllk33

V1

2022/12/30阅读:31主题:自定义主题1

Shiro从入门到实战(整合进SpringBoot)

前言

这篇文章主要是为了辅助记忆Shiro的,下面的代码主要复用了我看的一个视频里面的代码:https://www.bilibili.com/video/BV1Tf4y1w7Yo,所以有些地方如果不太了解的话可以去看看这个视频,不过这个视频有些地方可能有点绕,所以如果有shiro基础,只是想回顾一下的话,我觉得看这篇文章应该就可以很快回顾起来,如果没有shiro基础的话可能还是得跟着视频一起看。由于本人水平有限,所以可能有些地方写的不是很好,有发现什么问题的话,大家都可以指出来,共同进步!


一、Shiro是什么?

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

Shiro官网:http://shiro.apache.org/

二、Shiro 的功能介绍

1.基本功能点

image.png
image.png

2.功能点介绍

  • Authentication(认证):判断一个用户是否为合法用户的处理过程。最常用的身份认证方式是核对用户输入的用户名和密码是否与系统中存储的用户名和密码一致。还可以采用指纹和人脸识别等方式认证;
  • Authorization(授权):访问控制或权限验证,简单来说就是给不同的角色授予不同的权限,以及验证某个已认证的用户是否拥有某个权限;
  • Session Manager(会话管理):管理所有用户登录后的会话信息。用户登录后用Subject.getSession() 即可获取会话,在没有退出之前,用户的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web环境的;
  • Cryptography(加密):保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support(Web支持):可以非常容易的集成到Web环境;
  • Caching(缓存):用户登录后,其用户信息、拥有的角色/权限不必每次去查,直接从缓存中获取,这样可以提高效率;
  • Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me(记住我):非常常见的功能,即一次登录后,下次无需登录;

三、Shiro 的架构

1.Shrio的应用层面架构

image.png
image.png
  • Subject:任何直接与应用代码交互的对象。Subject代表了当前“用户”,这个用户不具体指代某个人,只要与当前应用交互的任何东西都是Subject,如第三方服务、网络爬虫、机器人等;所有Subject实例都必须被绑定到一个SecurityManager上,SecurityManager才是实际的执行者;
  • SecurityManager(安全管理器):与安全有关的操作都会与SecurityManager进行交互;管理着所有的Subject;它是Shiro的核心,负责与Shiro的其他组件进行交互,相当于SpringMVC中DispatcherServlet的角色;
  • Realm:担当Shiro和你的应用程序的安全数据之间的“桥梁”或“连接器”。Shiro可以从Realm中获取安全数据(如用户、角色、权限),即认证和授权等操作所需要的安全数据都需要从Realm中获得;

2.Shiro的核心架构

image.png
image.png
  • Subject:任何直接与应用代码交互的对象;
  • SecurityManager(安全管理器):所有与安全相关的操作都会与SecurityManager进行交互;它管理着所有的Subject;它是Shiro的核心,负责与Shiro的其他组件进行交互,相当于SpringMVC中DispatcherServlet的角色;
  • Authenticator:负责Subject认证,当一个用户尝试登录时,该逻辑被 Authenticator执行;且它是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过;
  • Authorizer(授权器):即访问控制器,用来决定主体(Subject)是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全数据;可以是JDBC实现,也可以是内存实现等等;它由用户提供,所以一般在应用中都需要实现自己的Realm;
  • SessionManager:管理所有用户登录后的会话信息。用户登录后用Subject.getSession() 即可获取会话,在没有退出之前,用户的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web环境的;
  • CacheManager(缓存控制器):管理如用户、角色、权限等信息的缓存;因为这些数据 基本上很少改变,放到缓存中后可以提高访问的性能;
  • Cryptography:密码模块,Shiro提供了一些常见的加密组件用于如密码加密/解密;

四、Shiro入门案例

以下是Shiro官网的一个入门小案例,供我们参考学习,我们主要关注其中的Quickstart.class以及shiro.ini中的部分代码就好。shiro.ini中主要放的就是用户、角色以及权限等数据,Quickstart.class就是简单的模拟一下认证及授权的过程,其中所用到的数据就来源于shiro.ini中,实际开发中我们的这些数据肯定是从数据库中获得。

1.shiro.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]
# '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 'delete' (action) the User(type) with
# license plate 'zhangsan' (instance specific id)
goodguy = User:delete:zhangsan

2.Quickstart.class

package com.m33.shiro.helloworld;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

    public static void main(String[] args) {

        // 创建SecurityManager(下面这三行看看就好)
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        // get the currently executing user
        // 获取当前的 Subject!调用 SecurityUtils.getSubject();
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        // 测试使用 Session 
        // 首先获取 Session: Subject的getSession()
        Session session = currentUser.getSession();
        session.setAttribute("someKey""aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("----> Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        // 测试当前的用户是否已经被认证. 即是否已经登录. 
        // 调用 Subject 的 isAuthenticated() ,若未认证,执行if里面的代码
        if (!currentUser.isAuthenticated()) {
         // 把用户名和密码封装为 UsernamePasswordToken 对象
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr""vespa");
            // rememberme
            token.setRememberMe(true);
            try {
             // 执行登录. 
                currentUser.login(token);
            } 
            // 若没有指定的账户, 则 shiro 将会抛出 UnknownAccountException 异常. 
            catch (UnknownAccountException uae) {
                log.info("----> There is no user with username of " + token.getPrincipal());
                return
            } 
            // 若账户存在, 但密码不匹配, 则 shiro 会抛出 IncorrectCredentialsException 异常。 
            catch (IncorrectCredentialsException ice) {
                log.info("----> Password for account " + token.getPrincipal() + " was incorrect!");
                return
            } 
            // 用户被锁定的异常 LockedAccountException
            catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            // 所有认证时异常的父类. 
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        // 成功登录
        log.info("----> User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        // 测试是否有某一个角色. 调用 Subject 的 hasRole 方法. 
        if (currentUser.hasRole("schwartz")) {
            log.info("----> May the Schwartz be with you!");
        } else {
            log.info("----> Hello, mere mortal.");
            return
        }

        //test a typed permission (not instance-level)
        // 测试用户是否具备某一个行为. 调用 Subject 的 isPermitted() 方法。 
        if (currentUser.isPermitted("lightsaber:weild")) {
            log.info("----> You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        // 测试用户是否具备某一个行为. User:delete:zhangsan: 表示允许删除User类型的zhangsan
        if (currentUser.isPermitted("User:delete:zhangsan")) {
            log.info("----> You are permitted to 'delete'(action) the User(type)with license plate (id) 'zhangsan'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to 'delete' the 'zhangsan' User!");
        }

        //all done - log out!
        // 执行登出. 调用 Subject 的 Logout() 方法. 
        System.out.println("---->" + currentUser.isAuthenticated()); // true
        currentUser.logout();
        System.out.println("---->" + currentUser.isAuthenticated()); // false
        
        System.exit(0);
    }
}

五、Shiro实战-整合进Spring Boot

这里先简单介绍一下我们这次实战所要完成的功能,其实主要就是对用户登录进行认证以及授予某些权限,让不同用户登录的时候看到不同资源。下面代入实际的应用场景给大家讲解一下: 首先我们有三个用户admin、manager、worker,它们分别具有role(角色):admin、manager、worker;admin角色拥有访问mobile、salary资源的权限,manager角色拥有访问salary资源的权限;woker角色不具备任何权限;当我们用admin用户登录的时候就可以查看mobile和salary,manager只能查看salary,worker则什么都查看不了

1.创建父工程shiroDemo

以下为父工程的pom.xml文件内容,需要引入以下依赖

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.3.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.6.0</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>
</dependencyManagement>

2.创建shiroAuth子模块(Maven)

以下为子工程的pom.xml文件内容,需要引入以下依赖,具体版本由父工程管理,这里只需添加我们需要用到的依赖即可

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-starter</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>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
    </dependency>
</dependencies>

3.代码结构讲解

下图是我们本次实战项目的代码结构,其中蓝色划掉的部分先暂时不用管它,后续我会讲解的

image.png
image.png

下面带大家先简单的了解下这些类所要实现的功能,后面再进行更详细的讲解

类名 功能
User 实体类,主要就是定义一些属性:用户名、密码、角色和权限等
MyRealm 一个自定义的Realm,主要负责获取认证和授权所需要的信息
ShiroConfig shiro的配置类,缓存、过滤器等功能在这里进行配置,在编译的时候便会先执行该配置类,后续登录认证或者授权的时候就会进到MyRealm中获取数据
LoginController 负责登录业务流程的控制
MobileController 负责获取mobile
SalaryController 负责获取salary
UserService 负责获取TestData中数据,返回给LoginController
MyPassWordEncoder 这是我们封装的对密码加盐加密的工具类
TestData 一些测试数据,主要是为了模拟UserService获取数据库中的数据,我们这次实战并没有连接数据库,为了测试使用才有了这个工具类,实际开发中这些数据需要存放在数据库中

4.各个类详细讲解

User

import java.util.List;
public class User {
    private String userName;
    private String userPass;
    private List<String> userRoles; //用户的角色
    private List<String> userPerms; //用户所具有的权限
    // 省略构造方法和get/set方法
    ...
}

MyRealm

import com.m33.shiroAuth.bean.User;
import com.m33.shiroAuth.service.UserService;
import com.m33.shiroAuth.utils.MyPassWordEncoder;
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.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration(value = "myRealm")
public class MyRealm extends AuthorizingRealm {

    private final Logger logger = LoggerFactory.getLogger(MyRealm.class);
    @Resource
    private UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("---------->entered method doGetAuthorizationInfo;");
        //拿到当前用户
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User)subject.getPrincipal();
        //写入当前用户的角色和权限信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(currentUser.getUserRoles());
        info.addStringPermissions(currentUser.getUserPerms());
        return info;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        logger.info("--------------->entered method doGetAuthenticationInfo;");
        //获得当前用户的token
        UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
        //从token中获得当前用户的用户名
        String username = userToken.getUsername();
        //模拟通过用户名获取数据库中用户密码
        User user = userService.getUserByUserName(username);
        if(null == user){
            //后续会抛出UnknownAccountException
            return null;
        }else{
            //MyPassWordEncoder.getEncodedPassword()是通过我们的工具类对用户密码进行加盐加密
            //正常是不需要这一步的,注册的时候就需要对密码进行加盐加密。因为我们没有模拟注册流程,
            // 所以现在密码是明文,我们才需要先把明文密码加盐加密,后续才能与输入的密码进行比对
            user.setUserPass(MyPassWordEncoder.getEncodedPassword(user.getUserPass()));
            //各参数含义分别为当前用户、用户密码(加密后的)、盐值(里面的“salt”可随意设置)、realm名
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getUserPass(),
                    ByteSource.Util.bytes("salt"), "myRealm");
            //密码验证逻辑由shiro完成,我们只需返回SimpleAuthenticationInfo对象即可。密码不匹配会抛出密码不匹配异常。
            //密码正确,会把user对象传入SecurityUtils.getSubject()。这些都是shiro帮我们做的。
            return authenticationInfo;
        }
    }
}

ShiroConfig

import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

 // 1、注册Realm对象,在MyRealm中有@Configuration(value = "myRealm"),已经注册好Realm对象了
 //所以以下代码就不需要了,但是我们需要知道配置shiro一般都需要以下三个步骤,我们可以当成模板来记忆,根据实际需求进行修改即可
 //@Bean
 //public Realm myRealm(){
 //     return new MyRealm();
 // }
    // 2、DefaultWebSecurityManager 
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") AuthorizingRealm myRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 通过HashedCredentialsMatcher 指定算法和加密次数
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(2);
        myRealm.setCredentialsMatcher(matcher);
        //设置缓存,使用缓存可以避免需要授权信息时频繁的调用数据库查询的问题。
  //原理很简单,只要在SecurityManager里注入CacheManager即可。
  //我们也可以自定义CacheManager的实现,可以是ehcache、redis等等。
  //实际开发中自定义RedisCache是很常见的,这里由于本人水平有限,就不演示如何用redis实现缓存了
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
        //最后要把我们的myRealm交给securityManager进行管理
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    //3、ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        /*
        配合Shiro的内置过滤器,参见DefaultFilter
           anon : 无需认证就可以访问
           authc : 必须认证才可以访问 对应@RequiresAuthentication注解
           user : 用户登录且记住我 才可以访问 对应@RequiresUser注解
           perms : 拥有某个资源才可以访问 对应@RequiresPermissions注解
           roles : 拥有某个角色才可以访问  对应@RequiresRoles注解
         */

        Map<String, String> filterChainDefinition = new HashMap<>();
        //配置antMatcher,跟SpringSecurity一样,可以配**,*,?
        // **:匹配路径中的零个或多个路径,如/admin/**将匹配/admin/a或/admin/a/b
        // *:匹配零个或多个字符串,如/admin将匹配/admin、/admin123,但不匹配/admin/1
        // ?:匹配一个字符,如/admin?将匹配/admin1,但不匹配/admin或/admin/
        filterChainDefinition.put("/mobile/**","perms[mobile]");
        filterChainDefinition.put("/salary/**","perms[salary]");
        filterChainDefinition.put("/main.html","authc");
        //配置登出过滤器
        filterChainDefinition.put("/logout","logout");
  //将配好的内容添加到拦截器中
        bean.setFilterChainDefinitionMap(filterChainDefinition);
        //设置登录页
        bean.setLoginUrl("/index.html");
        //设置成功后跳转的页面
        bean.setSuccessUrl("/main.html");
  //设置没有资源权限时跳转到的页面
        bean.setUnauthorizedUrl("/common/noauth");
        return bean;
    }
}

补充:Shiro中默认的过滤器

配置缩写 对应的过滤器 功能 例子
anon AnonymousFilter 指定url可以匿名访问 /admins/**=anon
authc FormAuthenticationFilter 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 /user/**=authc
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登录 /user/**=authcBasic
logout LogoutFilter 登出过滤器,配置指定url就可以实现退出功能,非常方便 filterChainDefinition.put("/logout","logout");
noSessionCreation NoSessionCreationFilter 禁止创建会话
perms PermissionsAuthorizationFilter 需要指定权限才能访问 /admin/**=perms[user:add:*],多个perms需要在[]中加“”,中间用,隔开
port PortFilter 需要指定端口才能访问 /admins/**=port[8080]
rest HttpMethodPermissionFilter 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 /admin/user/**=perms[user.method],其中method为post,get,delete等
roles RolesAuthorizationFilter 需要指定角色才能访问 /admins/**=roles["admin,user"]
ssl SslFilter 需要https请求才能访问
user UserFilter 需要已登录或“记住我”的用户才能访问

LoginController、MobileController、SalaryController

import com.m33.shiroAuth.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RequestMapping("/common")
@RestController
public class LoginController {

    private final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @PostMapping("/login")
    public Object login(String username,String password){
        Map<String,String> errorCode = new HashMap<>();
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        Subject user = SecurityUtils.getSubject();
        //如果用户已认证,直接打印already login
        if(user.isAuthenticated()){
            return "already login";
        }else{
            try{
            //用户未认证,下面进行认证
                user.login(token);
                user.getSession().setAttribute("currentUser",user.getPrincipal());
                return "login succeed";
            } catch (UnknownAccountException uae) {
            //捕获认证过程中出现的各种异常
                logger.info("There is no user with username of " + token.getPrincipal());
                errorCode.put("errorMsg","不存在的用户名");
            } catch (IncorrectCredentialsException ice) {
                logger.info("Password for account " + token.getPrincipal() + " was incorrect!");
                errorCode.put("errorMsg","密码不正确");
            } catch (LockedAccountException lae) {
                logger.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
                errorCode.put("errorMsg","账号被锁定");
            } catch(AuthenticationException authe){
                logger.info("Authentication error ",authe);
                errorCode.put("errorMsg",authe.getMessage());
            }
            return errorCode;
        }
    }
 //获取当前用户,用于前台页面展示用
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(){
        Subject subject = SecurityUtils.getSubject();
        return subject.getSession().getAttribute("currentUser");
    }
 //未授权时执行的方法
    @RequestMapping("/noauth")
    public String noAuth(){
        return "未经授权,无法访问。";
    }
}
--------------------------------------------------------------
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/mobile")
public class MobileController {
    /**
     * 代表一个查手机号的后台接口。
     * @return
     */

    @GetMapping("/query")
    public String query(){
        return "mobile";
    }
}
-------------------------------------------------------------
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/salary")
public class SalaryController {
    /**
     * 代表一个查薪水的后台接口。
     * @return
     */

    @RequiresPermissions("salary")
    @GetMapping("/query")
    public String query(){
    return "salary";
 }
}

UserService

import com.m33.shiroAuth.bean.User;
import com.m33.shiroAuth.utils.TestData;
import org.apache.commons.beanutils.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class UserService {

    @Resource
    private TestData testData;
 //这里的内容看看就好,主要是为了模拟获取数据库中的用户数据,实际开发中肯定是直接从数据库中获得
    public User getUserByUserName(String username){
        List<User> queryUsers = testData.getAllUser().stream().filter(user -> username.equals(user.getUserName())).collect(Collectors.toList());
        if(null != queryUsers && queryUsers.size()>0){
            try {
                return (User)BeanUtils.cloneBean(queryUsers.get(0));
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }else{
            return null;
        }
    }
}

TestData

import com.m33.shiroAuth.bean.User;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Component
public class TestData {

    private List<User> allUser;
    /**
     * 模拟数据库获取到的数据。
     * admin用户 拥有admin角色,拥有mobile和salary两个资源。
     * mobile用户,拥有mobile角色,拥有mobile资源。
     * worker用户,拥有worker角色,没有资源。
     * @return
     */

    public List<User> getAllUser(){
        if(null == allUser){
            allUser = new ArrayList<User>();
            allUser.add(new User("admin","admin",Arrays.asList("admin"),Arrays.asList("mobile","salary")));
            allUser.add(new User("manager","manager",Arrays.asList("manager"),Arrays.asList("salary")));
            allUser.add(new User("worker","worker",Arrays.asList("worker"),Arrays.asList("worker")));
        }
        return allUser;
    }
}

MyPassWordEncoder

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
/**
 * 负责密码加盐加密
 */

public class MyPassWordEncoder {

    public static String getEncodedPassword(String password){
        //各参数含义分别为加密方式、密码、盐值(可随意设置)、迭代次数(指反复加盐加密的次数)
        SimpleHash simpleHash = new SimpleHash("MD5",password, ByteSource.Util.bytes("salt"),2);
        return simpleHash.toString();
    }
}

以上便是全部类的代码讲解,前端代码这里就不去研究了,下面补充一下认证流程和授权流程

认证流程如下:

  • 1、首先调用Subject.login(token)进行登录,其会自动委托给SecurityManager
  • 2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
  • 3、Authenticator才是真正的身份验证者,ShiroAPI中核心的身份认证入口点,此处可以自定义插入自己的实现;
  • 4、Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
  • 5、Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

授权流程如下:

  • 1、首先调用Subject.isPermitted*/hasRole*接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;
  • 2、Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
  • 3、在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
  • 4、Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole*会返回true,否则返回false表示授权失败。

5.补充多Realm认证

当我们需要通过Shiro实现“用户名/手机号/邮箱”任意一个都可以登录的时候,我们就需要用到多Realm认证;我们要实现的效果就是通过用户名+密码或者手机号码+密码都可以实现登录

修改User类,加一个mobile参数,为了可以通过手机号登录

import java.util.List;

public class User {
    private String userName;
    private String userPass;
    private String mobile;//补充mobile属性
    private List<String> userRoles;
    private List<String> userPerms;
    ......
}

修改TestData,添加手机号

 public List<User> getAllUser(){
        if(null == allUser){
            allUser = new ArrayList<User>();
            allUser.add(new User("admin","admin","15777777777",Arrays.asList("admin"),Arrays.asList("mobile","salary")));
            allUser.add(new User("manager","manager","15888888888",Arrays.asList("manager"),Arrays.asList("salary")));
            allUser.add(new User("worker","worker","15999999999",Arrays.asList("worker"),Arrays.asList("worker")));
        }
        return allUser;
    }

修改UserService类,添加getUserByMobile方法

public User getUserByMobile(String mobile){
        List<User> queryUsers = testData.getAllUser().stream().filter(user -> mobile.equals(user.getMobile())).collect(Collectors.toList());
        if(null != queryUsers && queryUsers.size()>0){
            return queryUsers.get(0);
        }else{
            return null;
        }
    }

在config下添加MobileRealm,这就是上面那个代码结构图划掉的蓝色部分

import com.m33.shiroAuth.bean.User;
import com.m33.shiroAuth.service.UserService;
import com.m33.shiroAuth.utils.MyPassWordEncoder;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
//与MyRealm差不多,但是只需要认证流程即可,因为我们只是为了用手机号登录认证
@Configuration(value = "mobileRealm")
public class MobileRealm extends AuthenticatingRealm {

    private final Logger logger = LoggerFactory.getLogger(MobileRealm.class);
    @Resource
    private UserService userService;

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        logger.info("---->entered method doGetAuthenticationInfo;");
        UsernamePasswordToken usertoken = (UsernamePasswordToken)authenticationToken;
        String username = usertoken.getUsername();
        //只需要管用户名查询逻辑。模拟获取数据库中用户手机号
        User user = userService.getUserByMobile(username);
        if(null == user){
            return null//后续会抛出UnknownAccountException
        }else{
          user.setUserPass(MyPassWordEncoder.getEncodedPassword(user.getUserPass()));
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getUserPass(),
                    ByteSource.Util.bytes("salt"), "mobileRealm");
            return authenticationInfo;
        }
    }
}

最后,修改ShiroConfig的getDefaultWebSecurityManager方法

    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") AuthorizingRealm myRealm
            , @Qualifier("mobileRealm") Realm mobileRealm)
{
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(2);
        myRealm.setCredentialsMatcher(matcher);
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
        //创建多Realm认证器,设置认证策略,下面有对认证策略进行一个简单的介绍
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        securityManager.setAuthenticator(authenticator);
        //securityManager.setRealm(myRealm);securityManager.setRealms(Arrays.asList(myRealm,mobileRealm));
        return securityManager;
    }

补充:认证策略说明,Shiro提供了3个具体的 AuthenticationStrategy 实现 [1] AtLeastOneSuccessfulStrategy(默认实现):如果一个(或更多)验证成功,则整体的尝试被认为是成功的。如果没有一个验证成功,则整体失败。说白了就是,至少有一个Realm的验证是成功的算才认证通过,否则认证失败。 [2] FirstSuccessfulStrategy:第一个Realm成功验证返回的信息将被使用,其他的Realm将被忽略。如果没有一个Realm验证成功,则整体失败。 [3] AllSuccessfulStrategy:所有配置的Realm都必须验证成功才算认证通过,否则认证失败。

补充:Realm的继承关系

image.png 一般继承AuthorizingRealm(授权)即可;从图中我们可以看到它继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)。

分类:

后端

标签:

Java

作者介绍

M
MAllk33
V1