SpringBoot集成Shiro

一、Shiro 简介

Apache Shiro 是一个强大且易用的Java安全框架,能够用于身份验证、授权、加密和会话管理。

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

Shiro 功能:

  • 核心功能:

    • Authentication(认证):用户登录,身份识别。
    • Authorization(授权):授权和鉴权,处理用户和访问的目标资源之间的权限。
    • Session Management(会话管理):即 Session 的管理。
    • Cryptography(加密):用户密码加密。
  • 其他功能:

    • Web支持:可以非常容易集成到web应用程序中。
    • 缓存:缓存是Apache Shiro API中的第一级,以确保安全操作保持快速和高效。
    • 并发性:多线程环境完成认证和授权。
    • 测试:存在测试支持,可帮助您编写单元测试和集成测试,并确保代码按预期得到保障。
    • 运行方式:允许用户承担另一个用户的身份(如果允许)的功能,有时在管理方案中很有用。
    • 记住我:记住用户在会话中的身份,所以用户只需要强制登录即可。

Shiro 核心对象:

  • Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户等和软件交互的任何对象。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 框架的核心。
  • Realms:用于认证和授权,提供扩展点,使用者自行实现认证逻辑和授权逻辑。

二、SpringBoot集成Shiro

1:引入pom

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.7.1</version>
</dependency>

在GroupId为 org.apache.shiro下的ArtifaceId有好几个shiro-spring;shiro-springboot;shiro-springboot-starter。暂不清楚这三个之间的区别和联系,本文中引用的是 shiro-spring,使用的是当前(2021-07)最新版本 1.7.1。

2:增加配置类
配置类有两个:

  • 新建一个类 ShiroRealm ,此类继承 AuthorizingRealm ,是框架提供的扩展口,使用者通过重写doGetAuthenticationInfo 方法检验用户登录信息是否正确,并将用户信息存放到 session 中,此方法会抛出 AuthenticationException 异常,需要在统一异常处理中捕获此异常。通过重写 doGetAuthorizationInfo 方法为当前请求的用户赋予角色和权限信息,配合Shiro 提供的 @RequiresRoles 和 @RequiresPermissions 注解完成鉴权。

  • 新建一个类 ShiroConfig ,此类需要添加 @Configuration 注解,此类的作用是向应用程序上下文(俗称的容器)注入使用者添加的shiro配置和自定义功能。

    • 注入 SecurityManager,此对象是Shiro的核心对象之一,Shiro 框架提供了多种 DefaultSecurityManger可供使用,暂不清楚他们之间的区别,本文使用的是 DefaultWebSecurityManager 。通过 setRealm 将上一步新建的认证和授权配置类注入 SecurityManager。

    • 注入 ShiroFilterFactoryBean,此对象是一个过滤器,作用是配置一些默认页面和过滤规则。setLoginUrl是配置认证失败,默认要重定向的页面,可以是一个jsp页面,也可以是一个RESTFul接口。setFilterChainDefinitionMap是配置认证过滤规则,比如哪些URL不需要认证,接收一个 LinkedHashMap ,key为 url ,value 为认证策略,这里必须要吐槽认证策略没有设计成一个枚举。

      • logout:配置退出登录过滤器,其中的具体的退出代码Shiro已经替我们实现了,调用此接口后,页面会重定向到setLoginUrl配置的URL。
      • authc:配置需要认证的URL
      • anon:配置不需要认证的URL
    • 注入 authorizationAttributeSourceAdvisor 和 defaultAdvisorAutoProxyCreator ,如果不注入这两个对象,RequiresRoles 和RequiresPermissions 注解将无法使用。

ShiroRealm:

package com.naylor.shiro.config;


import com.alibaba.fastjson.JSON;
import com.naylor.shiro.dto.UserInfo;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 java.util.ArrayList;
import java.util.List;


/**
* @ClassName ShiroRealm
* @类描述 realm(领域、范围)不太清楚这里用这个单词是什么寓意。此类继承 AuthorizingRealm ,是框架给使用者留下的两个扩展点,doGetAuthenticationInfo 扩展登录认证的逻辑;doGetAuthorizationInfo 扩展授权鉴权的逻辑
*
* @Author MingliangChen
* @Email cnaylor@163.com
* @Date 2021-07-08 9:28
* @Version 1.0.0
**/
public class ShiroRealm extends AuthorizingRealm {


    /**
     * 授权
     * 在访问接口前,为当前登录用户赋予角色和权限
     * 实际应用中从数据库中查询用户拥有的角色和权限信息
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String principal = JSON.toJSONString(principalCollection);
        System.out.println(principal);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        UserInfo userInfo = (UserInfo)principalCollection.getPrimaryPrincipal();
       //根据用户名查询出用户角色和权限,并交给shiro管理。实际应用中用户角色和权限从数据库获取
        if (userInfo.getUserName().equals("cml")) {
            simpleAuthorizationInfo = buildUserCmlRolePermission();
        } else if (userInfo.getUserName().equals("admin")) {
            simpleAuthorizationInfo = buildUserAdminRolePermission();
        } else if (userInfo.getUserName().equals("hn")) {
            simpleAuthorizationInfo = buildUserHnRolePermission();
        }
        return simpleAuthorizationInfo;
    }


    /**
     * 登录认证
     * 保存用户信息到session中
     * 在调用登录接口后会进入到此方法(/common/singin)
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String authToken = JSON.toJSONString(authenticationToken);
        System.out.println("authToken:" + authToken);
        String userName = authenticationToken.getPrincipal().toString();
        UserInfo userInfo = this.getUserInfoByUserName(userName);
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), getName());
        return simpleAuthenticationInfo;
    }


    /**
     * 构造用户名为admin的用户的角色和权限
     * 实际应用中用户角色权限信息从数据库中获取
     * @return
     */
    private SimpleAuthorizationInfo buildUserAdminRolePermission() {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        List<String> roles = new ArrayList<>();
        roles.add("roleA");
        roles.add("roleB");
        roles.add("roleC");
        simpleAuthorizationInfo.addRoles(roles);
        List<String> permissions = new ArrayList<>();
        permissions.add("permissionsA");
        permissions.add("permissionsB");
        permissions.add("permissionsC");
        simpleAuthorizationInfo.addStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }


    /**
     * 构造用户名为cml的用户的角色和权限
     * 实际应用中用户角色权限信息从数据库中获取
     * @return
     */
    private SimpleAuthorizationInfo buildUserCmlRolePermission() {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        List<String> roles = new ArrayList<>();
        roles.add("roleA");
        roles.add("roleB");
        simpleAuthorizationInfo.addRoles(roles);
        List<String> permissions = new ArrayList<>();
        permissions.add("permissionsA");
        permissions.add("permissionsB");
        simpleAuthorizationInfo.addStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }


    /**
     * 构造用户名为hn的用户的角色和权限
     * 实际应用中用户角色权限信息从数据库中获取
     * @return
     */
    private SimpleAuthorizationInfo buildUserHnRolePermission() {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        List<String> roles = new ArrayList<>();
        roles.add("roleA");
        List<String> permissions = new ArrayList<>();
        permissions.add("permissionsAA");
        simpleAuthorizationInfo.addStringPermissions(permissions);
        simpleAuthorizationInfo.addRoles(roles);
        return simpleAuthorizationInfo;
    }


    /**
     * 获取用户信息根据用户名
     * 实际应用场景中是从数据库查询用户信息并根据需求组装 userInfo 对象
     *
     * @param userName
     * @return
     */
    private UserInfo getUserInfoByUserName(String userName) {
        UserInfo userInfo = new UserInfo().setId("112233445566778899").setUserName(userName).setRealName("陈明亮").setUserType(5).setNation("中国").setPassword("123456");
        return userInfo;
    }
}

ShiroConfig:

package com.naylor.shiro.config;


import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;


import java.util.HashMap;
import java.util.Map;
import java.util.Properties;


/**
* @ClassName ShiroConfgi
* @类描述 Shiro 配置
* @Author MingliangChen
* @Email cnaylor@163.com
* @Date 2021-07-08 9:24
* @Version 1.0.0
**/


@Configuration
public class ShiroConfig {




    /**
     * 注入安全管理
     * 为shiro框架核心对象,可注入不同的SecurityNamager对象,另外可根据实际需求通过securityManager的set方法自定义安全管理对象
     * @return
     */
    @Bean(name = "securityManager")
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(buildShiroRealm());
        return securityManager;
    }


    /**
     * 注入认证、授权
     * @return
     */
    @Bean(name = "shiroRealm")
    public ShiroRealm buildShiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }


    /**
     * 注入过滤器
     * 通过setLoginUrl配置认证失败,重定向的uri地址,可以是一个页面,也可以是一个RESTFul接口
     *
     * @param securityManager
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        Map<String, String> map = new HashMap<>();
        //退出登录
        map.put("/logout", "logout");
        //对所有URI认证
        map.put("/**", "authc");
        // 设置不用认证的URI
        map.put("/common/login", "anon");
        map.put("/common/singin", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);


        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //认证失败重定向URI
        shiroFilterFactoryBean.setLoginUrl("/common/login");


        return shiroFilterFactoryBean;
    }


    /**
     * 加入注解的使用,不加入这个注解不生效
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 加入注解的使用,不加入这个注解不生效
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }




}

3:增加全局异常处理
捕获异常,防止将tomcat的错误页面直接抛给用户。

配置文件中增加以下配置:
出现错误时, 直接抛出异常。这两个配置是为了让404异常正常抛出

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

GlobalException

package com.naylor.shiro.handler;


import com.naylor.shiro.dto.GlobalResponseEntity;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;


/**
* @ClassName GlobalException
* @类描述
* @Author MingliangChen
* @Email cnaylor@163.com
* @Date 2021-07-08 14:23
* @Version 1.0.0
**/


//@RestControllerAdvice("com.naylor")
@RestControllerAdvice()
@ResponseBody
@Slf4j
public class GlobalException {




    /**
     * 处理511异常
     * @param e
     * @return
     */
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<Object> handleAuthenticationException(AuthenticationException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "511",
                        e.getMessage() == null ? "认证失败" : e.getMessage()),
                HttpStatus.NETWORK_AUTHENTICATION_REQUIRED);
    }




    /**
     * 处理401异常
     * @param e
     * @return
     */
    @ExceptionHandler(AuthorizationException.class)
    public ResponseEntity<Object> handleAuthorizationException(AuthorizationException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "401",
                        e.getMessage() == null ? "未授权" : e.getMessage()),
                HttpStatus.UNAUTHORIZED);
    }




    /**
     * 处理404异常
     *
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, "404",
                        e.getMessage() == null ? "请求的资源不存在" : e.getMessage()),
                HttpStatus.NOT_FOUND);
    }


    /**
     * 捕获运行时异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Object> handleRuntimeException(RuntimeException e) {
        log.error("handleRuntimeException:", e);
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, "500",
                        e.getMessage() == null ? "运行时异常" : e.getMessage().replace("java.lang.RuntimeException: ", "")),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }


    /**
     * 捕获一般异常
     * 捕获未知异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleException(Exception e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "555",
                        e.getMessage() == null ? "未知异常" : e.getMessage()),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }


}

4:增加统一的RESTFul响应结构体
GlobalResponse:

package com.naylor.shiro.handler;


import com.alibaba.fastjson.JSON;
import com.naylor.shiro.dto.GlobalResponseEntity;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


import javax.annotation.Resource;


/**
* @ClassName GlobalResponse
* @类描述
* @Author MingliangChen
* @Email cnaylor@163.com
* @Date 2021-07-08 14:22
* @Version 1.0.0
**/


@RestControllerAdvice("com.naylor")
public class GlobalResponse  implements ResponseBodyAdvice<Object> {


    /**
     * 拦截之前业务处理,请求先到supports再到beforeBodyWrite
     * <p>
     * 用法1:自定义是否拦截。若方法名称(或者其他维度的信息)在指定的常量范围之内,则不拦截。
     *
     * @param methodParameter
     * @param aClass
     * @return 返回true会执行拦截;返回false不执行拦截
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        //TODO 过滤
        return true;
    }


    /**
     * 向客户端返回响应信息之前的业务逻辑处理
     * <p>
     * 用法1:无论controller返回什么类型的数据,在写入客户端响应之前统一包装,客户端永远接收到的是约定的格式
     * <p>
     * 用法2:在写入客户端响应之前统一加密
     *
     * @param responseObject     响应内容
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object responseObject, MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //responseObject是否为null
        if (null == responseObject) {
            return new GlobalResponseEntity<>("55555", "response is empty.");
        }
        //responseObject是否是文件
        if (responseObject instanceof Resource) {
            return responseObject;
        }
        //该方法返回值类型是否是void
        //if ("void".equals(methodParameter.getParameterType().getName())) {
        //  return new GlobalResponseEntity<>("55555", "response is empty.");
        //}
        if (methodParameter.getMethod().getReturnType().isAssignableFrom(Void.TYPE)) {
            return new GlobalResponseEntity<>("55555", "response is empty.");
        }
        //该方法返回值类型是否是GlobalResponseEntity。若是直接返回,无需再包装一层
        if (responseObject instanceof GlobalResponseEntity) {
            return responseObject;
        }
        //处理string类型的返回值
        //当返回类型是String时,用的是StringHttpMessageConverter转换器,无法转换为Json格式
        //必须在方法体上标注RequestMapping(produces = "application/json; charset=UTF-8")
        if (responseObject instanceof String) {
            String responseString = JSON.toJSONString(new GlobalResponseEntity<>(responseObject));
            return responseString;
        }
        //该方法返回的媒体类型是否是application/json。若不是,直接返回响应内容
        if (!mediaType.includes(MediaType.APPLICATION_JSON)) {
            return responseObject;
        }


        return new GlobalResponseEntity<>(responseObject);
    }
}

5:用户登录认证
用户信息都存放在 Subject 对象中,用户登录认证的过程只需调用其 login 方法即可,login方法内部会调用 doGetAuthenticationInfo 扩展点完成登录的认证。

package com.naylor.shiro.controller;


import com.naylor.shiro.dto.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.web.bind.annotation.*;


/**
* @ClassName LoginController
* @类描述
* @Author MingliangChen
* @Email cnaylor@163.com
* @Date 2021-07-08 9:51
* @Version 1.0.0
**/


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


    /**
     * 提示需要登录
     * @return
     */
    @GetMapping(value = "/login")
    public String login() {
        return "请登录";
    }


    /**
     * 登录
     * @param user
     * @return
     */
    @PostMapping("/singin")
    public String singIn(@RequestBody User user) {
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUserName(), user.getPassword());
        SecurityUtils.getSubject().login(usernamePasswordToken);
        return "登录成功";
    }






    @GetMapping("/error")
    public String error() {
        return "500";
    }
}

6:用户请求鉴权
通过 @RequiresRoles 和 @RequiresPermissions 注解的配合使用,完成对后端接口的鉴权。鉴权的逻辑其实就是从Shiro 中取出当前用户拥有的角色和权限,然后和RESTFul接口上面注解的角色和权限进行对比,如果包含那么就鉴权通过,允许访问,否则就抛出401异常。

鉴权原理:
debug 到AuthorizingRealm类的 isPermitted 方法,该方法接收两个参数,Permission为RESTFul接口上面添加的权限相关注解,AuthorizationInfo是当前请求用户拥有的角色和权限。鉴权的原理就是判断AuthorizationInfo 中是否包含Permission。

获取用户信息和session信息:
通过 SecurityUtils 工具类中的 getSubject方法获取用户的登录信息和sessionId

package com.naylor.shiro.controller;


import com.naylor.shiro.dto.UserInfo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
* @ClassName AnimalController
* @类描述
* @Author MingliangChen
* @Email cnaylor@163.com
* @Date 2021-07-08 13:49
* @Version 1.0.0
**/


@RestController
@RequestMapping("/animal")
public class AnimalController {




    @GetMapping("/cat")
    public String cat() {
        Subject subject = SecurityUtils.getSubject();
        UserInfo userInfo = (UserInfo) SecurityUtils.getSubject().getPrincipal();
        String sessionId = String.valueOf(SecurityUtils.getSubject().getSession().getId());
        return "cat";
    }


    @RequiresRoles({"roleA"})
    @RequiresPermissions("permissionsAA")
    @GetMapping("/fish")
    public String fish() {
        return "fish";
    }


    @RequiresRoles({"roleA", "roleB"})
    @GetMapping("/dog")
    public String dog() {
        return "dog";
    }


    @RequiresPermissions("permissionsC")
    @GetMapping("/tiger")
    public String tiger() {
        Boolean a = SecurityUtils.getSubject().hasRole("roleC");
        Boolean b = SecurityUtils.getSubject().isPermitted("permissionsC");
        return "tiger";
    }
}

7:测试
使用postman模拟用户请求进行测试。

a. 在没有登录的情况下,调用任何接口都会重定向到 /common/login 接口,该接口返回“请登录”。即使是访问一个不存在的页面也会重定向,因为我们在ShiroConfig 中配置的是全局认证。

b.调用登录接口登录,注意用户名需要和代码中写死的用户名一致

c.调用受限接口

admin 用户有 tiger 接口的权限,调用之后接口正常返回 tiger ; 没有 fish 接口的权限,调用之后返回 “Subject does not have permission [permissionsAA]”

8:总结
本文演示了SpringBoot集成 Shiro ,基于 Session 来管理用户会话,实现用户和web服务的认证和鉴权。Shiro 作为一个古老的框架,历史悠久,功能和拓展性也特别的强,如使用者可以自定义 SessionMode=HTTP 从而可以达到web服务横向扩容的目的;也可以结合 JWT 搭建无状态的web服务;还可以搭建 oauth2 。但是后两者并不推荐,在分布式系统和微服务应用中,推荐使用SpringBootSecutiryOauth2来搭建自己的授权服务。

邮箱:cnaylor@163.com
技术交流QQ群:1158377441
原文地址:https://www.cnblogs.com/Naylor/p/14990965.html