Spring Security + JWT + Swagger2 登录验证一套流程小结()

  本篇文章为你整理了Spring Security + JWT + Swagger2 登录验证一套流程小结()的详细内容,包含有 Spring Security + JWT + Swagger2 登录验证一套流程小结,希望能帮助你了解 Spring Security + JWT + Swagger2 登录验证一套流程小结。

  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 导入依赖

  

 dependencies 

 

   !-- web 依赖 --

   dependency

   groupId org.springframework.boot /groupId

   artifactId spring-boot-starter-web /artifactId

   /dependency

   !-- lombok 依赖 --

   dependency

   groupId org.projectlombok /groupId

   artifactId lombok /artifactId

   optional true /optional

   /dependency

   !-- mysql 依赖 --

   dependency

   groupId mysql /groupId

   artifactId mysql-connector-java /artifactId

   scope runtime /scope

   /dependency

   !-- mybatis-plus 依赖 --

   dependency

   groupId com.baomidou /groupId

   artifactId mybatis-plus-boot-starter /artifactId

   version 3.3.1.tmp /version

   /dependency

   !-- swagger2 依赖 --

   dependency

   groupId io.springfox /groupId

   artifactId springfox-swagger2 /artifactId

   version 2.7.0 /version

   /dependency

   !-- swagger 第三方 UI 依赖 --

   dependency

   groupId com.github.xiaoymin /groupId

   artifactId swagger-bootstrap-ui /artifactId

   version 1.9.6 /version

   /dependency

   !-- spring security --

   dependency

   groupId org.springframework.boot /groupId

   artifactId spring-boot-starter-security /artifactId

   /dependency

   !-- JWT 依赖 --

   dependency

   groupId io.jsonwebtoken /groupId

   artifactId jjwt /artifactId

   version 0.9.1 /version

   /dependency

   !-- commons-pool2 对象池依赖 --

   dependency

   groupId org.apache.commons /groupId

   artifactId commons-pool2 /artifactId

   /dependency

   /dependencies

  

 

  构建数据库表: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 存储的请求头

   tokenHeader: Authorization

   # JWT 加密使用的密钥

   secret: test-cnda-secret

   # JWT 的有效时间 (60*60*24)

   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;

  

 

  2. 配置 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 基本配置完成。

  3. 构建 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";

  
public String foundJWT(UserDetails userDetails) {

   String username = userDetails.getUsername();

   Map String, Object 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 String, Object 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);

  
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;

   * 返回响应结果

   private static RespBean result(long code, String message, Object obj) {

   return new RespBean(code, message, obj);

   返回成功响应

   public static RespBean success(String message) {

   return result(200, message, null);

   返回成功响应以及数据体

   public static RespBean success(String message, Object obj) {

   return result(200, message, obj);

   返回错误响应

   public static RespBean error(String message) {

   return result(500, message, null);

  

 

  4. 让 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 ? extends GrantedAuthority 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;

  

 

  5. 重写 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());

  

 

  6. 配置 JWT 的拦截器

  

public class JwtTokenFilter extends OncePerRequestFilter {

 

   @Autowired

   private JwtUtil jwtUtil;

   @Autowired

   private UserDetailsService service;

  
@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();

  

 

  7. 完善 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 SecurityContext securityContexts(){

   List SecurityContext 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 SecurityReference defaultAuth() {

   List SecurityReference 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;

  
private List SecurityScheme securitySchemes(){

   List SecurityScheme 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 从而完成免登录这一操作。

  以上就是Spring Security + JWT + Swagger2 登录验证一套流程小结()的详细内容,想要了解更多 Spring Security + JWT + Swagger2 登录验证一套流程小结的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: