看了认证流程,终于可以知道如何来修改Spring Security实现自己的JWT认证方式了。
粗看了一下,由于JWT的前提是用户名和密码需要通过认证,因此有很多种办法,比如:
- 由于返回JWT的前提是用户名和密码通过验证,就继承
UsernamePasswordAuthenticationFilter
,尝试验证的逻辑不变,在成功验证的执行中直接返回带有TOKEN响应头的响应。然后另外启动一个filter,就用于从头部信息中解码,之后根据解码的结果获取用户和权限,放到安全上下文中,设置成认证通过即可。
这种方法会覆盖掉默认的UsernamePasswordAuthenticationFilter
过滤器。
- 不通过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其实就是启动了一堆过滤器。
现在我们要实现的事情是两个:
- 向一个指定的地址post用户名和密码,获取200响应带有TOKEN的请求,或者401错误。
- 其他所有地址,都必须携带TOKEN访问,否则返回401错误。
于是整体的思路也有两个:
- 全部的逻辑都在过滤器中完成,因为
UsernamePasswordAuthenticationFilter
本身就有校验用户名和密码的功能,因此就利用(继承)这个过滤器。
- 将指定的请求放行到控制器中,在控制器中进行验证和返回JSON。
无论哪种方法,都需要要求访问其他地址必须携带TOKEN。
先来看第一种方法,既然有两个功能,就要求有两个过滤器:
- 过滤器1:仅针对
/api/auth
,POST进来用户名和密码,返回一个响应带有TOKEN
- 过滤器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对用户名和密码进行验证
- 验证之后在成功进行验证的方法中,设置上TOKEN和响应码,让过滤器1直接返回响应(不继续进行其他的过滤器)
- 客户端得到响应,将TOKEN保存,在请求的时候附带上TOKEN
- 过滤器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也不难。要做的事情有三个:
- 需要自定义
AuthenticationManager
域和构造器,以便将来传入
- 重写
attemptAuthentication
方法,逻辑不变,只是使用过滤器1覆盖的AuthenticationManager
域
- 重写
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的过滤器,细化起来,这个过滤器要做的事情是:
- 对于根路径,直接放行,由于单页面应用一般使用根路径用来返回HTML页面,有没有TOKEN都无所谓,因为访问根路径无需验证身份。
- 对于其他任意路径(前后端分离的情况下主要是一些REST API),都要检查请求头中的TOKEN,TOKEN有效才放行。
- 所谓放行,是指从JWT中解析出用户名和权限,然后组装一个通过验证的
UsernamePasswordAuthenticationToken
,并且设置在上下文中,让请求可以继续通过后边的用户密码验证。(至于权限也就是ROLE,那就看具体访问策略也就是最后一道守门的权限过滤器了)。
这个验证器的逻辑也就很清晰了,主要业务逻辑如下:
- 获取请求的目标URL地址
- 检查URL地址如果是根路径,直接
filterChain.doFilter
放行
- 如果URL地址不是根路径,尝试从请求头中获取TOKEN,然后进行TOKEN验证
- TOKEN不为空并且通过验证的情况下,设置
UsernamePasswordAuthenticationToken
并放行
来编写具体代码,这里Spring提供了一个类叫做OncePerRequestFilter
,顾名思义,就是每个请求执行一次的Filter。这个类继承GenericFilterBean
,GenericFilterBean
又实现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服务生效了,而用户权限也得到了正常使用。