Spring Security + JWT + Swagger2 登录验证小结2023-03-21 02:03:24

Spring Security + JWT + Swagger2 登录验证一套流程
主要是三个框架的集成配置,以及各个独立的配置(主要是 JWT + Security 的登录验证)。
流程:

构建 Spring Boot 基本项目,准备数据库表 User —— 用于存放登录实体类信息。
配置 Security 和 Swagger2 环境,确保没有什么问题。
构建 RespBean——公共返回实体类,JwtTokenUtil——JWT token 工具类,User——登录实体类
让 User 实现 UserDetails 接口,重写部分方法。
配置 Security 实现重写 UserDetailsService 方法,以及 PasswordEncoder——密码凭证器 并加上 @Bean 注解。这两个主要用于设置 Security 的认证。
构建 jwtAuthenticationTokenFilter 类——自定义 JWT Token 拦截器,并在 SecurityConfig 的授权方法中添加此拦截器。
在 Swagger2Config 配置类中,配置有关 Security 的 Token 认证。
启动项目查看代码是否准确。

  1. 构建 Spring Boot 基本项目,准备数据库——User
    项目子模块:authority-security,父模块已引入 Spring boot 依赖 2.3.0
    1.1 导入依赖
    org.springframework.boot spring-boot-starter-web org.projectlombok lombok true mysql mysql-connector-java runtime com.baomidou mybatis-plus-boot-starter 3.3.1.tmp io.springfox springfox-swagger2 2.7.0 com.github.xiaoymin swagger-bootstrap-ui 1.9.6 org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt 0.9.1 org.apache.commons commons-pool2

    复制代码
    构建数据库表:user
    create table user(
    id int primary key auto_increment,
    username varchar not null,
    password varchar not null,
    info varchar(200),
    enabled tinyint(1) default 1
    )

insert into user values(default,”admin”,”$2a$10$Himwt.wu3MPOLnNQ9YUH8O2quxgi7bMuomiNeFsVKRay87.qG5dgy”,”管理员 info …”,default)
复制代码
username:admin;password:123
配置 application.yml 文件参数:
server:
port: 8082

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dbtest16?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: admin
password: admin
hikari:
# 连接池名字
pool-name: DateHikari
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大事件,默认10分钟(600000)
idle-timeout: 180000
# 最大连接数:默认 10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,0 表示永久存活,默认 1800000(30 min)
max-lifetime: 1800000
# 连接超时事件 30 s
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1

MP 配置

mybatis-plus:
# 配置 Mapper 映射文件
mapper-locations: classpath:/mapper/Mapper.xml
# 实体类的别名包
type-aliases-package: com.cnda.pojo
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: false

MyBatis 的 SQL 打印是方法接口所在的包

logging:
level:
com.cnda.mapper: debug

JWT 配置

jwt:
# JWT 存储的请求头
tokenHeader: Authorization
# JWT 加密使用的密钥
secret: test-cnda-secret
# JWT 的有效时间 (606024)
expiration: 604800
# JWT 负载中拿到开头 规定
tokenHead: Bearer

复制代码
User 实体类代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String info;
private Boolean enabled;
}
复制代码

  1. 配置 Security 和 Swagger2 的配置
    先配置好这两个确保没有什么问题,因为重点是 JWT,这两个配置比较简单,当搭配了 JWT 之后,Swagger2 也需要与两者集成一些配置,这个后面再说,现在只配置基本设置。
    2.1 配置 SecurityConfig
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    super.configure(auth);
    } @Override
    public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers(
    “/hello”,
    // 下面是对静态资源以及 swagger2 UI 的放行。
    “/css/“, “/js/“,
    “/img/“, “/index.html”, “favicon.ico”, “/doc.html”, “/webjars/“,
    “/swagger-resources/“, “/v2/api-docs/“,
    “/ws/**”
    );
    } @Override
    protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    }
    }
    复制代码
    上面使用 WebSecurity 放行了 /hello 请求,在 LoginController 中。
    @RestController
    public class LoginController { @RequestMapping(“/hello”)
    public String hello(){
    return “Hello Word!”;
    }
    }
    复制代码
    这意味除了 localhost:8082/hello 会被放行,其他请求都会被 Security 拦截重定向到 /login(这个请求 Security 内部已经实现了包括相关页面)。
    2.2 配置 Swagger2Config
    @Configuration
    @EnableSwagger2
    public class Swagger2Config {
    @Bean
    public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
    .apiInfo(apiInfo()) // 配置 apiInfo
    .select() // 选择那些路径和api会生成document
    .apis(RequestHandlerSelectors.basePackage(“com.cnda.controller”)) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
    .paths(PathSelectors.any()) // 对所有路径进行监控
    .build(); } private ApiInfo apiInfo(){
    return new ApiInfoBuilder()
    .title(“在线接口文档”)
    .description(“在线接口文档”)
    .contact(new Contact(“cnda”,”http://localhost:8082/doc.html”,”xxx@xxx.com”))
    .build();
    }
    }
    复制代码
    运行效果:

修改一下 Rustful 风格,并加了一个 /hello1 请求,不放行,打印内容相同。

可以看到 Security 和 Swagger2 基本配置完成。

  1. 构建 JWT 工具类、公共响应对象
    JWT 工具类主要用于生成 JWT,判断 JWT 是否有效,刷新 JWT 等方法。
    公共响应对象——RespBean,返回的都已 JSON 格式返回。
    3.1 JwtUtil
    @Component
    public class JwtUtil {
    // 准备两个存放在荷载的内容
    private static final String CLAIM_KEY_SUB = “sub”;
    private static final String CLAIM_KEY_CREATE = “ibt”; // 提取 application.yml 中 JWT 的参数:
    // 1. expiration Long
    @Value(“${jwt.expiration}”)
    private Long expiration; // 2. secret String
    @Value(“${jwt.secret}”)
    private String secret; // 密钥 // 根据用户名构建 token
    public String foundJWT(UserDetails userDetails) {
    String username = userDetails.getUsername();
    Map claims = new HashMap<>();
    claims.put(CLAIM_KEY_SUB, username);
    claims.put(CLAIM_KEY_CREATE, new Date());
    return foundJWT(claims);
    } // 根据荷载 map 构建 token
    private String foundJWT(Map claims) {
    return Jwts.builder()
    .setClaims(claims)
    .setExpiration(getExpiration()) // 过期时间
    .signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥
    .compact();
    } // 判断 token 是否有效
    public boolean validateToken(String token,UserDetails userDetails){
    // 从 token 中获取 username 与 userDetails 中的username 对比
    String username = getUsernameInToken(token);
    // 判断 username 是否一致以及 token 是否过期
    return username.equals(userDetails.getUsername()) && !isExpired(token);
    } // 判断 token 是否过期
    // true 过期 false 没过期
    private boolean isExpired(String token) {
    Date expiration = getClaimsInToken(token).getExpiration();
    return expiration.before(new Date());
    }
    // 从 token 中提取荷载信息
    public Claims getClaimsInToken(String token){
    Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }catch (Exception e){ e.printStackTrace(); } return claims; } // 从 token 中提取用户名信息
    public String getUsernameInToken(String token){
    String username;
    try {
    username = getClaimsInToken(token).getSubject();
    }catch (Exception e){
    username = null;
    }
    return username;
    }
    // token 是否能刷新
    public boolean tokenCanRef(String token){
    return !isExpired(token); // 有效地 token 才能被刷新
    }
    // 刷新 token
    public String refToken(String token){
    Claims claimsInToken = getClaimsInToken(token);
    claimsInToken.put(CLAIM_KEY_CREATE,new Date());
    return foundJWT(claimsInToken);
    } // 设置过期时间
    private Date getExpiration() {
    return new Date(System.currentTimeMillis() + expiration * 1000);
    }
    }
    复制代码
    3.2 RespBean 公共返回对象
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class RespBean {
    private long code;
    private String message;
    private Object obj; /**/*
    返回成功响应以及数据体
    */
    public static RespBean success(String message, Object obj) {
    return result(200, message, obj);
    } /*
    返回错误响应
    */
    public static RespBean error(String message) {
    return result(500, message, null);
    }
    }
    复制代码
  2. 让 User 实体类实现 UserDetails 的方法成为 Security 验证的用户核心主体
    由于 Security 框架的性质,自定义授权和认证时,一般情况下会自定义 UserDetails。
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private String info;
    private Boolean enabled; @Override
    public Collection getAuthorities() { // 权限角色
    return null;
    } @Override
    public boolean isAccountNonExpired() {
    return false;
    } @Override
    public boolean isAccountNonLocked() {
    return false;
    } @Override
    public boolean isCredentialsNonExpired() {
    return false;
    } @Override
    public boolean isEnabled() { // 这里数据库实现了该字段,直接用即可
    return this.enabled;
    }
    }
    复制代码
  3. 重写 UserDetailsServer 和 PasswordEncoder
    5.1 重写 UserDetailsServer
    这个类就只有一个方法:
    loadUserByUsername(UserDetails details),该方法用于根据用户名加载用户信息,用作于 Security 的后续认证,同时也可以用一个类去实现该接口,这里为了方便,同时也是 Lambda 表达式。
    注意:这里的 UserMapper 没有代码展示了,就一个根据用户名查询用户信息的 SQL。
    @Resource
    private UserMapper mapper;

@Bean
@Override
protected UserDetailsService userDetailsService() {
return username -> {
User user = mapper.find(username);
if (user!=null){
return user;
}
throw new UsernameNotFoundException(“用户名或密码不正确”);
};
}
复制代码
5.2 PasswordEncoder——密码凭证器
这个类主要用于验证表单提交的密码是否和 重写之后的 UserDetailsServer 得到的 UserDetails 中的加密密码一致。
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
复制代码
5.3 配置到 SecurityConfig 的认证中
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(encoder());
}
复制代码

  1. 配置 JWT 的拦截器
    public class JwtTokenFilter extends OncePerRequestFilter { @Autowired
    private JwtUtil jwtUtil; @Autowired
    private UserDetailsService service; @Value(“${jwt.tokenHeader}”)
    private String tokenHeader; @Value(“${jwt.tokenHead}”)
    private String tokenHead; @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    // 获取请求头中的指定的值
    String headerToken = httpServletRequest.getHeader(tokenHeader);
    // 保证 header中的 token 不为 null,且以指定字串开头——Bearer
    if (headerToken!=null && headerToken.startsWith(tokenHead)){
    // 截取有效 token
    String jwtToken = headerToken.substring(tokenHead.length());
    String username = jwtUtil.getUsernameInToken(jwtToken);
    // 判断 UserDetails 中的用户主体是否为null
    if (username!=null && SecurityContextHolder.getContext().getAuthentication() == null){
    // SecurityContextHolder.getContext().getAuthentication() == null 代表着此时 Security 中没有登录的用户主体
    // 此时可以使用有效地 jwtToken 进行用户认证
    UserDetails userDetails = service.loadUserByUsername(username);
    // 判断 token 是否有效
    if (jwtUtil.validateToken(jwtToken,userDetails)){
    // 如果有效则使用 token 中的信息进行登录
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
    // 根据请求设置 Details,包含了部分请求信息和主体信息。具体效果不清楚…坑
    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
    // 将 authenticationToken 设置到 SecurityContext 中
    SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(httpServletRequest,httpServletResponse); }
    }
    复制代码
    6.1 将 JWT 拦截器设置到 SecurityConfig 的授权方法中。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // 由于我们使用的是 JWT 令牌的形式来验证用户,所以可以将 csrf 防御关闭
    // JWT 能有效防止 csrf 攻击,强行使用 csrf 可能导致令牌泄露
    http.csrf()
    .disable()
    // 基于 token,不需要使用 Session 了
    .sessionManagement() // Session 管理
    // 管理 Session 创建策略
    // ALWAYS, 总是创建HttpSession
    // NEVER, 只会在需要时创建一个HttpSession
    // IF_REQUIRED, 不会创建HttpSession,但如果它已经存在,将可以使用HttpSession
    // STATELESS; 永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
    .authorizeRequests() // 授权请求
    // 除了上面的请求,其他所有请求都需要认证
    .anyRequest()
    .authenticated()
    .and()
    // 禁止缓存
    .headers()
    .cacheControl(); // 自定义拦截器 JWT 过滤器
    http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 将过滤器按照一定顺序加入过滤器链。

}

@Bean
public JwtTokenFilter jwtTokenFilter() {
return new JwtTokenFilter();
}
复制代码

  1. 完善 LoginController 请求,运行项目。
    LoginController
    @RestController
    public class LoginController {
    @Autowired
    private UserService service; @GetMapping(“/hello”)
    public String hello(){
    return “Hello Word!”;
    }
    @GetMapping(“/hello1”)
    public String hello1(){
    return “Hello1 Word!”;
    } @PostMapping(“/login”)
    public RespBean loginUser(@RequestBody User user, HttpServletRequest request){
    return service.login(user.getUsername(),user.getPassword(),request);
    }
    }
    复制代码
    UserService,使用的时 MVC 模式,所以只展示实现类:
    @Service
    public class UserServiceImpl implements UserService { @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtUtil jwtUtil; @Value(“${jwt.tokenHead}”)
    private String tokenHead; @Override
    public RespBean login(String username, String password, HttpServletRequest request) {
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    if (userDetails==null || !passwordEncoder.matches(password,userDetails.getPassword())){
    return RespBean.error(“用户名或密码错误!”);
    }
    if (!userDetails.isEnabled()){
    return RespBean.error(“用户状态异常!”);
    } UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities()); String jwt = jwtUtil.foundJWT(userDetails); SecurityContextHolder.getContext().setAuthentication(token); Map<String,String> msg = new HashMap<>(); msg.put("tokenHead",tokenHead); msg.put("token", jwt); return RespBean.success("登录成功!",msg); }
    }

复制代码
7.1 完善 Swagger2Config 配置
由于 JWT 的加入,所以 Swagger2 的方法请求也是需要带入 JWT 令牌,提供了 Security 的全局认证。
只展示了修改的部分。
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 配置 apiInfo
.select() // 选择那些路径和api会生成document
.apis(RequestHandlerSelectors.basePackage(“com.cnda.controller”)) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
.paths(PathSelectors.any()) // 对所有路径进行监控
.build()
// 添加和 Security 相关的配置。
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}

// 以下方法相对于给 Swagger 添加了一个在 Security 的全局授权,并且以正则的形式设置了授权的请求 url

/**
* securityContexts
* 请求体内容
/ private List securityContexts(){ List securityContexts = new ArrayList<>(); securityContexts.add(getContextByPath(“/hello/.“));
return securityContexts;
}

// 通过正则表达式来设置哪些路径
// 通过 Path 获取到对应的 SecurityContext
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex)) // 按照 String 的 matches 方法进行匹配
.build();
}

/**
* 配置默认的全局鉴权策略;其中返回的 SecurityReference 中,reference 即为 ApiKey 对象里面的name,保持一致才能开启全局鉴权
* @return SecurityReference
*/
private List defaultAuth() {
List references = new ArrayList<>();
// scope 参数:
AuthorizationScope authorizationScope = new AuthorizationScope(“global”,”accessEverything”);
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
references.add(new SecurityReference(“Authorization”,authorizationScopes));
return references;

}

/**
* securitySchemes
* 安全体方案
*/
private List securitySchemes(){
List apiKeys = new ArrayList<>();
// 设置请求头信息
apiKeys.add(new ApiKey(“Authorization”,”Authorization”,”Header”));
return apiKeys;
}
复制代码
修改的部分直接 CV 大法即可。
7.2 运行项目查看效果:

可以看到利用 Swagger2 的调试,返回 JWT Token 令牌成功!
{
“code”: 200,
“message”: “登录成功!”,
“obj”: {
“tokenHead”: “Bearer”,
“token”: “eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlidCI6MTY3Nzk4NzIwNjgyMSwiZXhwIjoxNjc4NTkyMDA2fQ.p_GUqevx8gvCK2txxeEX-RQFm69yDCxCYNlZbeHgVIizSUDO6gaT3a2jGXvzXqofH2uxkQBgN4WfeSIlGydiNA”
}
}
复制代码
将令牌设置到 Swagger2 中

这样之前的 /hello1 就可以请求成功了:

说明 Swagger2 设置 JWT 也成功了,每次发送请求,头部都会携带 JWT 令牌。
总结
还是对 Security 不太熟悉,Swagger2 的配置比较固定
JWT 主要也是两个点:

JWT Token Utile 工具类,主要用于管理 JWT 令牌。
JWT Token Filter JWT 拦截器,这个就是 Security 和 JWT 的集成了,以及请求发来的时候解析 JWT 从而完成免登录这一操作。

« »