渐进式 shiro - shiro + jwt+salt (三)

渐进式-springboot-shiro-jwt

完整版:springboot + shiro + jwt + salt.

放弃 Cookie ,Session ,使用 JWT 进行鉴权,完全实现无状态鉴权

shiro 完整流程以及集成:

  1. 用户访问登录接口 /login, 用户输入登录账号和密码被封装成 UsernamePasswordToken 对象,然后调用 subject.login() 方法
  2. shiro 立即进入用户认证过程,进入执行 UserRealm doGetAuthenticationInfo()方法代码块。
  3. 用户登录成功后,登录接口返回生成得 token.
  4. 访问其他所有需要携带token得接口,此处以支付接口 /pay为例子,必须用户登录成功访问并在请求头中添加 token。
  5. 控制哪些请求需要在请求头中添加 token,哪些请求不需要 token 可以直接访问(比如/login)的方式叫做:jwt(JSON Web Token),无状态鉴权机制
@Slf4j
@RestController
public class UserController {
    
    

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public AjaxResult loginUser(@RequestBody UserEntity userVo) {
    
    
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userVo.getUsername(), userVo.getPassword());
        usernamePasswordToken.setRememberMe(true);

        try {
    
    
            subject.login(usernamePasswordToken);
            log.info("登录成功");
        } catch (AuthenticationException ae) {
    
    
            return AjaxResult.error("账号或密码不正确");
        }

        UserEntity userEntity = (UserEntity) subject.getPrincipal();
        userEntity.setToken(JwtUtils.generateToken(userEntity.getUsername(),JwtUtils.secret));

        return AjaxResult.success(userEntity);

    }

    @GetMapping("/pay")
    public AjaxResult payWithToken() {
    
    
        return AjaxResult.success("this Uri need token");
    }
}

引入依赖

JWT 方案有许多种,这个网站列举了所有的常用方案,在这里我们选择 java-jwt

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.10.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
<!--    jwt工具    -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.2.0</version>
</dependency>

ShiroConfig 配置类

对于 HTTP 请求,springboot 默认使用 Servlet 来处理,而 shiro 的过滤器正是基于 Servlet 实现,因此所有的 Http 请求,都会执行设定好的过滤器方法.

在前面的文章中,其实我们已经使用了过滤器,使用的都是 shiro 提供的现成过滤器名称缩写:shiro 常见过滤器

在 ShiroConfig ShiroFilterFactoryBean 中, 对过滤器进行统一的设定.代码变动位置有 2 处

@Configuration
public class ShiroConfig {
    
    

    /**
     * 默认web安全管理器
     *
     * @return {@link DefaultWebSecurityManager}
     */
        @Bean
    public SessionManager sessionManager(){
    
    
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 关闭 cookie 验证
        sessionManager.setSessionIdCookieEnabled(false);
        // 关闭 session 验证
        sessionManager.setSessionValidationSchedulerEnabled(false);
        return sessionManager;
    }

    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager());

        /*
         * 关闭shiro自带的session
         * 文档: http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * `shiroFilter`:过滤器
     *
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
    
    
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 自定义过滤器 ================ 变动1
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("myFilter", new MyFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // 定义过滤链
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 对静态资源设置匿名访问
        filterChainDefinitionMap.put("/index.html", "anon");
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/static/**","anon");

        // 登录,不需要拦截的访问
        filterChainDefinitionMap.put("/login", "anon");
        // 错误页面无需认证
        filterChainDefinitionMap.put("/error","anon");


        // !!! 其他所有请求使用自定义的过滤器 myFilter 来处理  ================ 变动2
        filterChainDefinitionMap.put("/**","myFilter")
        // filterChainDefinitionMap.put("/**","authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

}

filterChainDefinitionMap.put("/",“myFilter”)** 用来设置其他所有请求包括 /pay 会执行即将创建的自定义过滤器 myFilter

自定义 UserRealm

在 ShiroConfig 中,规定了只有登录接口(subjet.login())会使用 UserRealm.需要携带token得接口与UserRealm 毫无关系

ShiroRealm 设计为可拔插模块,而 Realm 又分为两部分:认证,授权。两个单词非常相似。

  • 授权 doGetAuthorizationInfo: 处理角色是否能够访问相应的 web service 相关信息
  • 认证 doGetAuthenticationInfo: 处理角色登录相关信息
public class UserRealm extends AuthorizingRealm {
    
    

    /**
     * shiro默认机制是 通过token的类型来确认是否由当前realm来处理当前收到的登录请求
     * 因此 在这里限定只有通过 UsernamePasswordToken这个类,调用的login接口可以使用此Realm认证
     * 
     */
    @Override
    public boolean supports(AuthenticationToken token) {
    
    
        return token instanceof UsernamePasswordToken;
    }

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        return null;
    }
    /**
     * 认证
     * AuthenticationToken 接口提供了2方法,getPrincipal() 返回的用户的账号信息,getCredentials() 返回的是密码信息。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        // 登陆时传入的用户名,密码
        UsernamePasswordToken accessToken = (UsernamePasswordToken) authenticationToken;
        // 获取用户名
        String username = (String) authenticationToken.getPrincipal();

        // 查询用户
        UserEntity userEntity = userService.getOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, accessToken.getUsername()));

        // 组装并返回
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userEntity, // 用户
                accessToken.getPassword(), // 密码
                ByteSource.Util.bytes(salt),// byte类型 salt
                "anyRealmName"  // realm name .  getName()
        );
        return authenticationInfo;
    }
}

自定义过滤器 MyFilter

当访问 /pay时,请求会执行 MyFilter 中代码块。需要注意的是,当你修改了代码后,要先执行以下 /login 登录,复制返回得 token,再访问此接口


/**
 * $$ 代码的执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin
 *
 * @author [email protected]
 * @version 1.0.0
 * @date 2022/10/27 17:18
 **/
@Slf4j
public class MyFilter extends BasicHttpAuthenticationFilter {
    
    
    /**
     * 过滤器拦截请求的入口方法,所有请求都会进入该方法
     * 1. 返回true则允许访问
     * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
     * @param request     请求
     * @param response    响应
     * @param mappedValue 映射值
     * @return boolean
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
        log.info("允许访问 - 周期");
        // 所有自定义过滤器得请求都需要携带token
        return false;
    }

    /**
     * isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
     *
     * 所有过滤器处理请求都可以在 isAccessAllowed中处理,或者在 onAccessDenied 中处理
     *
     * 由于过滤器在controller前运行,token过期时,抛出的异常不会全局异常捕获,而在 onAccessDenied 是可以精准抛出此异常。
     *
     * 所以在 onAccessDenied 中处理
     *
     * @param request  请求
     * @param response 响应
     * @return boolean
     * @throws Exception 异常
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    
    
        log.info("拒绝访问 - 周期");
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);

        // 请求头不含token
        if(StringUtils.isEmpty(token)) {
    
    
            responseError(response,HttpStatus.UNAUTHORIZED.value(),"token不能为空");
            return false;
        }
        // 请求头含有 token
        String username = JwtUtils.getUserName(token);
        if(!JwtUtils.verify(token, username, JwtUtils.secret)){
    
    
            responseError(response,HttpStatus.UNAUTHORIZED.value(),"token无效");
            return false;
        }

        if(JwtUtils.isExpired(token)) {
    
    
            responseError(response,HttpStatus.UNAUTHORIZED.value(),"token已失效,请重新登录!");
            return false;
        }
        log.info(String.valueOf("verify"));
        return true;
    }


    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
    
    
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        fillCorsHeader(httpRequest,httpResponse);

        // 过滤options方法。跨域时会首先发送一个option请求
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
    
    
            httpResponse.setStatus(HttpStatus.OK.value());
            return true;
        }
        return super.preHandle(request, response);
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
    
    
        //从header中获取token
        String token = httpRequest.getHeader("Authorization");

        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
    
    
            token = httpRequest.getParameter("Authorization");
        }

        return token;
    }

    /**
     * 跨域请求的解决方案之一
     *
     * @param request  请求
     * @param response 响应
     */
    protected void fillCorsHeader(HttpServletRequest request, HttpServletResponse response) {
    
    
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        response.setHeader(
                "Access-Control-Allow-Headers",
                request.getHeader("Access-Control-Request-Headers")
        );
    }

    protected void responseError(ServletResponse response,int code,String errorMsg) throws IOException  {
    
    
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        AjaxResult r = AjaxResult.error(HttpStatus.UNAUTHORIZED.value(), "token不能为空");

        String json = new ObjectMapper().writeValueAsString(r);
        httpResponse.getWriter().print(json);
    }
}

JwtUtils

工具类都定义为静态方法,使用时可以避免注入此工具类。

package com.mock.water.core.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.mock.water.modules.system.user.entity.UserEntity;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Author [email protected]
 * @Date 2022/11/9 11:14
 */
@Slf4j
public class JwtUtils {
    
    
    /**
     * 密钥
     */
    public static String secret = "ifredom123456";
    /**
     * 到期时间 7天
     */
    public static long expire = 7*1000*60*60*24;

    /**
     * 创建 token
     */
    public static String generateToken(String username, String secret) {
    
    
        Date now = new Date();
        Date date = new Date(now.getTime() + expire * 1000);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
    }

    /**
     * 验证 token
     */
    public static boolean verify(String token, String username, String secret) {
    
    
        try {
    
    
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (Exception e) {
    
    
            log.error("token 无效 {}", e.getMessage());
            return false;
        }
    }

    /**
     * token是否过期
     *
     * @return true:过期
     */
    public static boolean isExpired(String token) {
    
    
        DecodedJWT jwt = JWT.decode(token);
        return System.currentTimeMillis() > jwt.getExpiresAt().getTime();
    }

    /**
     * 从 token中获取字段
     *
     * @return token中包含的填入字段
     */
    public static String getClaim(String token, String claim) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
    
    
            log.error("error:{}", e.getMessage());
            return null;
        }
    }

    public static String getUserName(String token) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
    
    
            log.error("error:{}", e.getMessage());
            return null;
        }
    }

    public String getSecret()  {
    
    return secret;}
    public void setSecret(String secret) {
    
    this.secret = secret;}
    public void setExpire(long expire) {
    
    this.expire = expire;}
    public long getExpire() {
    
    return expire;}
}

Realm 中 SimpleAuthenticationInfo 详解

在认证功能中会使用到 Shiro 封装好的 SimpleAuthenticationInfo 类.

SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        userEntity, // 用户
        accessToken.getPassword(), // 密码
        ByteSource.Util.bytes(salt),// byte类型 salt
        "anyRealmName"  // realm name .  getName()
);
  • 第一个参数,可以传入用户名 username,也可以传入从数据库查询得到的 userEntity 实体对象。(shiro 会自动调用实体的 getUserName()去获取 username字段值)建议传入 userEntity

    传入 userEntity(实体),subject.getPrincipal() 得到的是 userEntity(实体);

    传入 username(字符串),subject.getPrincipal() 得到的是字符串。

  • 第二个参数,传入的是用户登录时输入的 password。它被装入 SimpleAuthenticationInfo类返回后,会与 UsernamePasswordToken 中的 password 进行对比。匹配上了就表明验证通过,匹配不上就报异常。

    需要注意,网上很多文章说的是传入数据库中的密码,数据库中应该存放的是 username 和 salt,或者加密之后的密码,一定不能是明文密码.

  • 第三个参数(可选参数),salt 盐。此参数目的:用于对密码进行加密以及对比,防止用户的密码相同。

    具体来说就是:假如两个用户的密码都是 123456, Shiro 在比较 数据库中获取的 passwordUsernamePasswordToken 中的 password 的值时,默认会先调用这个类 new SimpleHash(String algorithmName, Object source)对密码执行一次 MD5 哈希算法得到字符串,然后使用哈希化后的两个字符串进行比较,这两字符串相同,那么就表示密码相同。(shiro 并不会上来就直接比较 2 个密码原文,会分别哈希算法转换一次后,对比转换后的值)

    所以问题就来了, 如果两个用户密码相同,在没有 salt 的情况下,他们的哈希值是一样的,就会造成错误判断。加盐后就可以避免不同用户的密码不一样。

  • 第四个参数:当前 realm 对象的 beanName, 可以通过 getName() 获取

认证方法 doGetAuthenticationInfo 入参 AuthenticationToken 详解

认证方法 doGetAuthenticationInfo() 有一个入参,类型为 AuthenticationToken

protected AuthenticationInfo doGetAuthenticationInfo(
    AuthenticationToken authenticationToken
) throws AuthenticationException {
    
    }

参数 AuthenticationToken 是一个接口,它拥有 2 个实现类和 2 个继承接口,关系如下。

参数 authenticationToken 从哪里来呢?

它是在登陆 login() 时,我们创建一个 UsernamePasswordToken 对象然后传入的,传入的必须是一个AuthenticationToken的实现类.(经过测试,此处并不能传入 new BearerToken()这个实现类)

@PostMapping("/login")
public void loginUser(@Validated @RequestBody UserVo userVo, BindingResult bindingResult) {
    
    
    Subject subject = SecurityUtils.getSubject();

    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userVo.getUsername(), userVo.getPassword());
    usernamePasswordToken.setRememberMe(true);

    // 传入
    subject.login(usernamePasswordToken);
}

从继承关系图可以看出,为什么它可以向下转型

UsernamePasswordToken accessToken = (UsernamePasswordToken) authenticationToken;

过滤链 ShiroFilterChainDefinition

在配置类中,shiro 提供了一个简单得封装类 ShiroFilterChainDefinition,可以将过滤连提取为一个单独得方法,代码看上去更为舒适.

@Configuration
public class ShiroConfig {
    
    

    ......

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
    
    
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 自定义过滤器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("myFilter", new MyFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // 定义过滤链
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    
    
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        // 不需要拦截的访问
        // 对静态资源设置匿名访问
        chainDefinition.addPathDefinition("/index.jsp", "anon");
        chainDefinition.addPathDefinition("/login.jsp", "anon");
        chainDefinition.addPathDefinition("/favicon.ico**", "anon");
        chainDefinition.addPathDefinition("/captcha/captchaImage**", "anon");
        chainDefinition.addPathDefinition("/static/**","anon");

        // 登录,
        chainDefinition.addPathDefinition("/login", "anon");
        // 注册
        chainDefinition.addPathDefinition("/register", "anon");
        // 错误页面
        chainDefinition.addPathDefinition("/error","anon");
        // 登出,shiro 自动清除 session
        chainDefinition.addPathDefinition("/logout","logout");
        // druid连接池的角色控制,只有拥有admin角色的admin用户可以访问,不理解可以先不管
        chainDefinition.addPathDefinition("/druid/**","authc, roles[admin]");

        // 其余资源都交给 MyFilter 这个过滤器处理
        chainDefinition.addPathDefinition("/**","myFilter");
        return chainDefinition;
    }
}

DefaultShiroFilterChainDefinition内部使用 LinkedHashMap 实现 . HashMap 是无序的,LinkedHashMap 将会按序加载。最后添加了 chainDefinition.addPathDefinition(“/**”, “authc”), 如果使用 HashMap 将会优先加载了此配置,导致其他配置失效。

常见过滤器

配置缩写 对应的过滤器 功能
anon AnonymousFilter 指定 url 可以匿名访问
authc FormAuthenticationFilter 基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure)

AuthorizationInfo 授权

shiro 对权限授权划分为:角色 和 资源

  • 角色

  • 资源权限
    ————————————————
    注解方式授权

  • @RequiresAuthentication: 使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 必须在当前 session 中已经过认证。

  • @RequiresGuest: 使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 可以是“gust”身份,不需要经过认证或者在原先的 session 中存在记录。

  • @RequiresPermissions: 当前 Subject 需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前 Subject 不具有这样的权限,则方法不会被执行。

  • @RequiresRoles: 当前 Subject 必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天 Subject 不同时拥有所有指定角色,则方法不会执行还会抛出 AuthorizationException 异常。

  • @RequiresUser:当前 Subject 必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

    实例

issuses

  1. ShiroFilterFactoryBean 方法名取名为 shiroFilterFactoryBean()
  2. logout功能。如果再拦截器链中配置了 logout,那么不要再定义 controller。只定义其中一个
@Configuration
public class ShiroConfig {
    
    
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    
    
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/logout","logout");

        return chainDefinition;
    }
}

====================

@RestController
public class UserController {
    
    
    @GetMapping("/logout")
    public void logout(){
    
    
        SecurityUtils.getSubject().logout();
        System.out.println("登出");
    }
}

3.shiro 中出现 does not support authentication token

/**
    * 大坑!,必须重写此方法,不然Shiro会报错
    */
@Override
public boolean supports(AuthenticationToken token) {
    
    
    return token instanceof JwtToken;
}
  1. Filter 中的依赖注入.在配置的过程中, 一个坑点就是: 如果你希望将你的 JWTUtil 工具类(或许其它)通过依赖注入的方式注入到你的自定义 filter 中, 绝对不能使用 Autowired 注解, 因为 filter 的初始化早于 beans 的初始化, 因此是无法将 bean 通过 autowired 注入到 filter 类中的. 解决方法是: 通过为 filter 类增添构造函数, 在构造函数中传入 ApplicationContext, 然后在通过 context 获取 bean.
public class Myfilter{
    
    

    private JwtUtils jwtUtils;
    public LoginFilter(ApplicationContext context) {
    
    
        this.util = context.getBean(MyJWTUtil.class);
    }

}

changelog

  • 2021 年发布 shiro1.8 带来了质的飞跃,对于本文的需求来说,最利好的包括两点
  • 一是增加了对 SpringBoot 自动装配机制的支持;
  • 二是增加了 BearerHttpAuthenticationFilter 这个默认过滤器,从而让 Jwt 的整合获得了原生级的适配性。以上两项特性大大精简了我们的配置工作,且让当前网络上所有的教程都落后于时代。(包括官网和英文网络,搜到的教程基本都是旧版本的配置。)

阅读资料

猜你喜欢

转载自blog.csdn.net/win7583362/article/details/127899311