Spring Security JWT认证实现--使用过滤器实现

Spring Security JWT认证实现--使用过滤器实现

看了认证流程,终于可以知道如何来修改Spring Security实现自己的JWT认证方式了。 粗看了一下,由于JWT的前提是用户名和密码需要通过认证,因此有很多种办法,比如: 由于返回JWT的前提是用户名和密码通过验证,就继承UsernamePasswordAuthenticationFilter

看了认证流程,终于可以知道如何来修改Spring Security实现自己的JWT认证方式了。 粗看了一下,由于JWT的前提是用户名和密码需要通过认证,因此有很多种办法,比如:
  1. 由于返回JWT的前提是用户名和密码通过验证,就继承UsernamePasswordAuthenticationFilter,尝试验证的逻辑不变,在成功验证的执行中直接返回带有TOKEN响应头的响应。然后另外启动一个filter,就用于从头部信息中解码,之后根据解码的结果获取用户和权限,放到安全上下文中,设置成认证通过即可。 这种方法会覆盖掉默认的UsernamePasswordAuthenticationFilter过滤器。
  2. 不通过filter拦截登录的post请求,而是直接送到控制器,由控制器返回JSON字符串或者响应头。其他路径则全部需要鉴别token。
两种办法相比之下,其实是第二种办法比较灵活一些,如果愿意的话,可以在响应头中设置好TOKEN之后,通过JSON返回额外数据,方便前端获取。当然,本身filter也可以在内部设置只对某些路径进行过滤,本身就很灵活。 先来编写纯过滤器实现的JWT验证。

1 整体思路

在Spring Boot启动的时候,通过启动日志中可以查看全部的过滤器,我这里启动了一个只包含web和security组件的Spring Boot项目,然后查看日志:
2019-06-05 12:53:46.432  INFO 44468 --- [           main] o.s.s.web.DefaultSecurityFilterChain     :
Creating filter chain: any request, [
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4f8d86e4,
//向ThreadLocal和Session中写入和读取Authentication并组装安全上下文的过滤器
org.springframework.security.web.context.SecurityContextPersistenceFilter@493ac8d3,
//给头部加上x-开头的头部信息的过滤器
org.springframework.security.web.header.HeaderWriterFilter@41da3aee,
//CSRF过滤器
org.springframework.security.web.csrf.CsrfFilter@12f49ca8,
//实现logout的过滤器
org.springframework.security.web.authentication.logout.LogoutFilter@7978e022,
//验证用户名密码的过滤器
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@60dd0587,
//生成默认登录页面
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@45aca496,
//生成默认登出页面
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@5f631ca0,
//针对BasicAuth认证的过滤器
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@3b7eac14,
//这个类的作用主要是用于用户登录成功后,重新恢复因为登录被打断的请求
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@67531e3a,
//包装request,实现servlet api的一些接口方法isUserInRole、getRemoteUser
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1945113f,
//匿名过滤器,前边都认证没成功,添加一个匿名用户
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1697f2b3,
org.springframework.security.web.session.SessionManagementFilter@64920dc2,
//捕获异常进行跳转
org.springframework.security.web.access.ExceptionTranslationFilter@443faa85,
//验证权限,也就是Authorities,对应ROLE,也是最后一道关口
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@56928e17
]
如果和只添加了web组件的SpringBoot项目对比,可以看到Spring Security其实就是启动了一堆过滤器。 现在我们要实现的事情是两个:
  1. 向一个指定的地址post用户名和密码,获取200响应带有TOKEN的请求,或者401错误。
  2. 其他所有地址,都必须携带TOKEN访问,否则返回401错误。
于是整体的思路也有两个:
  1. 全部的逻辑都在过滤器中完成,因为UsernamePasswordAuthenticationFilter本身就有校验用户名和密码的功能,因此就利用(继承)这个过滤器。
  2. 将指定的请求放行到控制器中,在控制器中进行验证和返回JSON。
无论哪种方法,都需要要求访问其他地址必须携带TOKEN。 先来看第一种方法,既然有两个功能,就要求有两个过滤器:
  1. 过滤器1:仅针对/api/auth,POST进来用户名和密码,返回一个响应带有TOKEN
  2. 过滤器2:放行根目录,对于其他路径验证TOKEN

2 继承内置过滤器的编写

这个方式参考了https://dev.to/keysh/spring-security-with-jwt-3j76这篇指导。 首先来简单设计一下,提交验证的路径是/api/auth。这个路径只能接收POST请求来返回TOKEN。然后根目录可以被直接放行。其他任何路径,包括/api/auth的GET请求,都需要被保护。 过滤器1的实现可以继承UsernamePasswordAuthenticationFilter,因为这个过滤器是Spring Security提供,有一个方法可以设置接收POST验证的地址,所以可以很方便的依靠继承这个类来进行操作。 不过继承之后,是不能直接使用AbstractAuthenticationProcessingFilter提供的方法获取AuthenticationManager,必须自己写一个变量覆盖父类的私有变量。 那么到哪里去获取AuthenticationManager的实现呢? 好在WebSecurityConfig继承的WebSecurityConfigurerAdapter有一个方法:
public AuthenticationManager authenticationManagerBean() throws Exception {
    return new WebSecurityConfigurerAdapter.AuthenticationManagerDelegator(this.authenticationBuilder, this.context);
}
只要在构造器中设置好一个AuthenticationManager参数,在配置文件中添加过滤器的时候,直接依靠WebSecurityConfigurerAdapter的这个方法就可以了。 这样我们的逻辑是:
  1. 过滤器1对用户名和密码进行验证
  2. 验证之后在成功进行验证的方法中,设置上TOKEN和响应码,让过滤器1直接返回响应(不继续进行其他的过滤器)
  3. 客户端得到响应,将TOKEN保存,在请求的时候附带上TOKEN
  4. 过滤器2过滤剩下所有地址

2.1 准备工作

创建一个新的Spring Boot项目,选上Web,Security和Thymeleaf即可,再添加上JWT的依赖。 然后编写一个简单的控制器来返回页面,让页面上可以显示出用户名。
package cc.conyli.jwtfilter.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class BaseController {

    @GetMapping
    private String homePage() {
        return "home";
    }
}
然后是一个简单的首页:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="https://www.thymeleaf.org"
    xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

</body>
</html>
由于要使用JWT,根据之前的学习,有一系列内容比如KEY之类的需要生成。把相关的内容都写在一个配置类,实际上,可以写一个工具类用于提供JWT的服务。
package cc.conyli.jwtfilter.jwt;

import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Base64;

@Component
public class JWTUtils {

    private static String BASE_SECRET_STRING = "dSF*F*()SD)(*()9032190898gfsd980*(F*(DS(*()*#@*(*#()!@*()#*(!)@";

    private static final Key KEY = Keys.hmacShaKeyFor(BASE_SECRET_STRING.getBytes());

    public static String AUTH_LOGIN_URL = "/api/auth";

    public static String TOKEN_HEADER = "Authorization";

    public static String TOKEN_ISSUER = "conyli.cc";

    public static Key getKey() {
        return JWTUtils.KEY;
    }

    public String getKeyString() {
        return Base64.getEncoder().encodeToString(KEY.getEncoded());
    }
}
这里简单写了一个类,用于放置一些配置属性,比如POST用户的地址,头部名称,一些需要设置给TOKEN的信息,以及最关键的,提供一个固定的根据字符串生成的KEY。这样在使用到KEY的时候就可以使用这个工具类。 之后就可以来编写过滤器1了。

2.2 过滤器1-继承的过滤器

了解了认证流程后,其实这个过滤器1也不难。要做的事情有三个:
  1. 需要自定义AuthenticationManager域和构造器,以便将来传入
  2. 重写attemptAuthentication方法,逻辑不变,只是使用过滤器1覆盖的AuthenticationManager
  3. 重写successfulAuthentication方法,这个方法的逻辑改变很大,不再是添加Authentication对象并放行了。下边具体解释。
按照上边的步骤逐步编写,先是第一步:
package cc.conyli.jwtfilter.filter;

import cc.conyli.jwtfilter.jwt.JWTUtils;
import io.jsonwebtoken.Jwts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    // 第一步,创建域,这里无法使用Spring框架的@Autowired自动注入,因为过滤器的请求还没有到框架里边,也就没有进到IOC容器
    private AuthenticationManager authenticationManager;
    // 依然是第一步,创建构造器
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        //这条代码是这个种类的过滤器特有的,限制了这个过滤器过滤的URL。当然,这个逻辑也可以自行编写或者进行具体配置。
        //这里就将其限定为"/api/auth"
        setFilterProcessesUrl(JWTUtils.AUTH_LOGIN_URL);
    }
}
这一步主要是为了能够正常使用AuthenticationManager对象,以及监听/api/auth路径。 然后第二步,重写attemptAuthentication方法:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    //抽象类提供了两个方法用户获取用户名和密码,其实内部就是request.getParameter方法
    String username = this.obtainUsername(request);
    String password = this.obtainPassword(request);

    //和原来方法的逻辑一样,创建UsernamePasswordAuthenticationToken对象
    //注意这里是两参数构造器,构造器其中是this.setAuthenticated(false),说明该身份未通过认证
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);

    //和原来方法的逻辑一样,但是必须要换成自己的域,而不是原来方法的this.getAuthenticationManager().authenticate(authRequest)语句
    return authenticationManager.authenticate(authenticationToken);
}
第二步重写的这个方法,内部的机制和原来是一样的。要去调用AuthenticationManager去实际进行验证,之后的验证流程就是之前说过的,会找到Provider验证,Provider验证通过之后会创建一个新的Authentication对象,然后AbstractAuthenticationProcessingFilter会通过successfulAuthentication方法设置认证后的对象到安全上下文中。 所以第三步,就是来重写这个successfulAuthentication方法:
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    //从Authentication对象中取出User信息
    User user = ((User) authResult.getPrincipal());

    //将权限列表转换为一个数组列表,方便转换成JSON
    //user.getAuthorities()返回的是user对象中的Collection<GrantedAuthority>,authority.getAuthority()则返回权限字符串名称,最后得到一个字符串列表
    List<String> roles = user.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.toList());
    //把用户名,TOKEN发行者,过期日期,权限列表字符串写入到claims中,然后使用唯一的Key进行签名,最后生成JWT字符串
    String JWTTOken = Jwts.builder()
            .setSubject(user.getUsername())
            .setIssuer(JWTUtils.TOKEN_ISSUER)
            .setExpiration(new Date(System.currentTimeMillis() + 3600*1000))
            .claim("role", roles)
            .signWith(JWTUtils.getKey())
            .compact();
    //故意设置一个特殊的状态码看看
    response.setStatus(255);
    //响应头设置上Authorization信息
    response.setHeader(JWTUtils.TOKEN_HEADER, JWTTOken);

    //在原始的代码中,这里还去调用了成功之后的handler,从而继续向下验证或者跳转到刚才登录成功的URL。
    //由于我们的目的是返回token,这里不调用任何东西。则最终不会调用filterChain.doFilter(httpServletRequest, httpServletResponse);
}
注意这里的User对象,是org.springframework.security.core.userdetails.User对象,因为需要从Authentication中取出,将其转换成实现了UserDetails接口的User对象。 这个过滤器的逻辑很简单,先去认证,认证成功后,生成TOKEN附加在请求上直接返回。而用户名和密码验证,走的还是Spring原来相同的逻辑。

2.3 配置Spring Security

现在还需要配置一下Spring Security,引入这个过滤器,关闭csrf和允许跨域:
package cc.conyli.jwtfilter.config;

import cc.conyli.jwtfilter.filter.JWTAuthenticationFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
            .and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilter(new JWTAuthenticationFilter(authenticationManager()))
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
前边允许跨域,关闭CSRF,然后指定"/api/auth"允许任何访问(实际上针对这个路径的访问不会进入到控制器,即使验证成功也是立刻返回)。 之后的红字比较关键,是给过滤器传入了一个authenticationManager(),这就是自定义过滤器无法获得AuthenticationManager对象的解决方案。 最后是关闭session。

2.4 验证过滤器的工作

现在就可以来启动项目了。 在启动的过程中,注意观察此时的过滤器:
2019-06-05 19:47:45.836  INFO 40768 --- [           main] o.s.s.web.DefaultSecurityFilterChain     :
Creating filter chain: any request, [
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@38f77cd9,
org.springframework.security.web.context.SecurityContextPersistenceFilter@367f0121,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.web.filter.CorsFilter@4a8e6e89,
org.springframework.security.web.authentication.logout.LogoutFilter@600f5704,
cc.conyli.jwtfilter.filter.JWTAuthenticationFilter@6fbb4061,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@441b8382,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@77114efe,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@63d5874f,
org.springframework.security.web.session.SessionManagementFilter@7d7cac8,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@11e355ca]
可以看到,相比一开始空的项目,CSRF过滤器,自定义登录登出页面过滤器都不见了。新增加了一个CorsFilter。还有一个显著的变化就是自己写的JWTAuthenticationFilter替代了原来的UsernamePasswordAuthenticationFilter。 现在我们没有写自己的UserDetailsService,所以Spring Security在控制台里打印了一个随机的密码。 现在可以尝试来获取JWT了,启动POSTMAN或者其他工具,向http://localhost:8080/api/auth发送POST请求,设置Body为form-data或者x-www-form-urlencoded都可以,然后填写username的值是user,password的值是随机密码,之后点击发送。 如果用户名和密码都正确,可以看到响应中出现:
Authorization →eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ1c2VyIiwiaXNzIjoiY29ueWxpLmNjIiwiZXhwIjoxNTU5NzM5MzE3LCJyb2xlIjpbXX0.Yi-abfa787KLFDGpXYAtyy-IN8IWYV8Y0qc8cVUz_540ZWc0Kr0aPXSxlMY2xdC-
说明已经成功拿到了TOKEN。此外还可以看到状态码是刚才设置的255。如果发送AJAX的是Vue,此时就可以从Header中取出这个KEY然后放到Vuex中。

3 全局验证TOKEN过滤器的编写

现在要编写的过滤器就是总体思路里边提到的过滤器2,也就是全局验证TOKEN的过滤器,细化起来,这个过滤器要做的事情是:
  1. 对于根路径,直接放行,由于单页面应用一般使用根路径用来返回HTML页面,有没有TOKEN都无所谓,因为访问根路径无需验证身份。
  2. 对于其他任意路径(前后端分离的情况下主要是一些REST API),都要检查请求头中的TOKEN,TOKEN有效才放行。
  3. 所谓放行,是指从JWT中解析出用户名和权限,然后组装一个通过验证的UsernamePasswordAuthenticationToken,并且设置在上下文中,让请求可以继续通过后边的用户密码验证。(至于权限也就是ROLE,那就看具体访问策略也就是最后一道守门的权限过滤器了)。
这个验证器的逻辑也就很清晰了,主要业务逻辑如下:
  1. 获取请求的目标URL地址
  2. 检查URL地址如果是根路径,直接filterChain.doFilter放行
  3. 如果URL地址不是根路径,尝试从请求头中获取TOKEN,然后进行TOKEN验证
  4. TOKEN不为空并且通过验证的情况下,设置UsernamePasswordAuthenticationToken并放行
来编写具体代码,这里Spring提供了一个类叫做OncePerRequestFilter,顾名思义,就是每个请求执行一次的Filter。这个类继承GenericFilterBeanGenericFilterBean又实现Filter。可以看到最终也是一个Filter。 点进OncePerRequestFilter的源码可以看到其doFilter的内部是执行的doFilterInternal方法,这个doFilterInternal方法是一个抽象方法。 我们自己的类就继承OncePerRequestFilter,然后实现doFilterInternal方法,直接来看完整的类:
package cc.conyli.jwtfilter.filter;

import cc.conyli.jwtfilter.jwt.JWTUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class JWTTokenFilter extends OncePerRequestFilter {

    //重写实际进行过滤操作的doFilterInternal抽象方法
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        //获取URL
        String targetPath = httpServletRequest.getRequestURI();
        //如果访问的是根路径,直接放行
        if (targetPath.equals("/")) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }

        //尝试获取TOKEN
        String JWTToken = httpServletRequest.getHeader("Authorization");
        //如果TOKEN不为空,尝试解析JWTToken并且组装Authentication实现对象即UsernamePasswordAuthenticationToken
        if (JWTToken != null) {
            Authentication authentication = null;
            //尝试解析的过程中如果出错,就设置一个特殊的响应码,然后直接返回,不再执行后续操作
            try {
                authentication = getAuthenticationFromToken(JWTToken);
            } catch (Exception ex) {
                logger.info(ex.toString());
                httpServletResponse.setStatus(468);
                return;
            }
            //如果成功拿到UsernamePasswordAuthenticationToken,设置到安全上下文上,然后放行
            SecurityContextHolder.getContext().setAuthentication(authentication);
            filterChain.doFilter(httpServletRequest, httpServletResponse);

        } else {
            //如果TOKEN不存在,直接返回401错误,表示未认证
            httpServletResponse.setStatus(401);
        }

    }

    private Authentication getAuthenticationFromToken(String token) throws Exception {
        //尝试验证JWT
        //解析第一部,获取解析后的前两部分的拼合对象
        Jws<Claims> jws = Jwts.parser().setSigningKey(JWTUtils.getKey()).parseClaimsJws(token);
        //从claims中获取放入的用户名
        String username = jws.getBody().getSubject();
        //从role字符串数组转换成权限对象
        ArrayList<String> roleStrings = (ArrayList<String>)jws.getBody().get("role");
        List<GrantedAuthority> authorities = roleStrings.stream().map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());
        //组装UsernamePasswordAuthenticationToken并返回这个认证对象,是三参数构造器,说明认证通过
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
        return authenticationToken;
        }
}
这个类的整体业务逻辑就是刚才所说的业务逻辑,将从token解析出一个UsernamePasswordAuthenticationToken的过程封装到了一个方法中,向外抛异常。 为了方便检测,抓住异常的时候给了一个特殊的468响应码,如果是不携带TOKEN则返回401未认证错误。

3.1 配置Spring Security

过滤器写好了,还必须将其配置到Spring Security中,由于我们有了两个自定义的过滤器,需要规定顺序,所以配置如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .cors()
            .and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/api/auth").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilter(new JWTAuthenticationFilter(authenticationManager()))
            .addFilterAfter(new JWTTokenFilter(),JWTAuthenticationFilter.class)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
这里注意,这个过滤器是针对全部路径的,如果把这个过滤器放在JWTAuthenticationFilter的前边,那么TOKEN将无法获取,所以将其配置在其后发挥作用。

3.2 简单的UserDetailsService和角色设置

为了方便测试,可以简单的写一个每次都会产生一个变化的用户名的UserDetailsService来替代随机生成密码,还赋予一个权限,然后编写一个简单的控制器,来验证一下TOKEN不会影响权限: 控制器如下,根据用户权限显示不同的信息:
package cc.conyli.jwtfilter.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@org.springframework.web.bind.annotation.RestController
@RequestMapping("/rest")
public class RestController {

    @GetMapping("/superuser")
    public String helloSuperUser() {
        return "Hello authenticated superuser";
    }

    @GetMapping("/user")
    public String helloUser() {
        return "Hello authenticated user";
    }

    @GetMapping("/admin")
    public String helloAdmin() {
        return "Hello authenticated admin";
    }
}
UserDetailsService如下:
package cc.conyli.jwtfilter.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@Component
public class UserService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //生成一个包含三个字符串的列表,注意这里需要加ROLE_前缀
        List<String> roles = new ArrayList<>();
        roles.add("ROLE_USER");
        roles.add("ROLE_ADMIN");
        roles.add("ROLE_SUPERUSER");

        //随机挑选列表中的一个字符串生成权限对象
        Random random = new Random();
        int index = random.nextInt(3);
        String role = roles.get(index);
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(role));

        //把用户输入的用户名,空的密码和随机选出的权限对象,包装成一个User对象并返回
        //密码这里是Spring Security 5的新写法,表示明文密码123
        User user = new User(s, "{noop}123", authorities);

        logger.info("UserDetailsService返回的对象是:" + user.toString());

        return user;
    }
}
最后再修改一下Spring Security配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .cors()
            .and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/api/auth").permitAll()
            .antMatchers("/rest/admin").hasRole("ADMIN")
            .antMatchers("/rest/superuser").hasRole("SUPERUSER")
            .antMatchers("/rest/user").hasRole("USER")
            .anyRequest().authenticated()
            .and()
            .addFilter(new JWTAuthenticationFilter(authenticationManager()))
            .addFilterAfter(new JWTTokenFilter(),JWTAuthenticationFilter.class)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

4 完成

再次启动服务,现在可以通过POSTMAN向/api/auth发请求,用户名可以任意起,密码只要是123,就可以得到TOKEN。 同时可以在控制台查看UserDetailsService返回的用户的权限。 然后在POSTMAN里,向具有不同权限的URL带着TOKEN发送GET请求来试验一下,就会发现JWT服务生效了,而用户权限也得到了正常使用。
LICENSED UNDER CC BY-NC-SA 4.0
Comment