看了认证流程,终于可以知道如何来修改Spring Security实现自己的JWT认证方式了。
粗看了一下,由于JWT的前提是用户名和密码需要通过认证,因此有很多种办法,比如:
- 由于返回JWT的前提是用户名和密码通过验证,就继承
UsernamePasswordAuthenticationFilter
,尝试验证的逻辑不变,在成功验证的执行中直接返回带有TOKEN响应头的响应。然后另外启动一个filter,就用于从头部信息中解码,之后根据解码的结果获取用户和权限,放到安全上下文中,设置成认证通过即可。
这种方法会覆盖掉默认的UsernamePasswordAuthenticationFilter
过滤器。 - 不通过filter拦截登录的post请求,而是直接送到控制器,由控制器返回JSON字符串或者响应头。其他路径则全部需要鉴别token。
两种办法相比之下,其实是第二种办法比较灵活一些,如果愿意的话,可以在响应头中设置好TOKEN之后,通过JSON返回额外数据,方便前端获取。当然,本身filter也可以在内部设置只对某些路径进行过滤,本身就很灵活。
先来编写纯过滤器实现的JWT验证。
- 1 整体思路
- 2 继承内置过滤器的编写
- 2.1 准备工作
- 2.2 过滤器1-继承的过滤器
- 2.3 配置Spring Security
- 2.4 验证过滤器的工作
- 3 全局验证TOKEN过滤器的编写
- 3.1 配置Spring Security
- 3.2 简单的UserDetailsService和角色设置
- 4 完成
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服务生效了,而用户权限也得到了正常使用。
写的很好,源代码在那能获取,参考一下