spring security(二)-自定义认证(包含前后端分离认证)

1.界面

  • 简介

    通过之前的章节实现了自定义登录认证,但是登录的界面是框架提供的,有时候更希望是通过自定义登录界面,接下来就来实现自定义登录界面

  • 配置

    1. 复制spring-security-config-account项目,修改名字为spring-security-login-page

    2. 修改启动类内容如下

      package com.briup.security;
      
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.context.annotation.Bean;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.crypto.password.PasswordEncoder;
      
      @SpringBootApplication
      class SpringSecurityLoginPageApplication {
      
         public static void main(String[] args) {
             SpringApplication.run(SpringSecurityLoginPageApplication.class, args);
         }
      
      
         @Bean
         public PasswordEncoder passwordEncoder() {
             return new BCryptPasswordEncoder();
         }
      }
      
      
    3. 修改pom.xml文件,修改的内容如下

      image-20210120102150914
    4. 测试

      访问地址:http://127.0.0.1:9999/hello/test

      跳转到默认提供的登录界面

      image-20210120102332475
  • 自定义登录界面

    要想实现自定义登录界面需要以下两步:

    • 撰写登录界面
    • 修改security配置类

    接下来就来挨个实现这两个步骤


    撰写登录界面

    src/mainresources新建static目录,并且在该目录下新建login.html,内容如下:

    
    
    
        
        登录界面
    
    
        

    登录

    用户名:
    密码:

    了解(start)

    表单的用户名和密码的name属性值必须为usernamepassword,具体原因如下:

    • UsernamePasswordAuthenticationFilter中,获取用户名和密码

    • 该过滤器获取用户名密码则根据usernamepassword获取,如下

      image-20210120103920614

      同时还限制了其请求方式为post

    了解(end)


    修改配置类

    找到配置类,进行修改,修改的内容如下:

    package com.briup.security.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("myDetailService")
        private UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin() // 表示使用表单进行登录
                .loginPage("/login.html") // 表示登录界面地址
                .loginProcessingUrl("/user/login") //表单登录地址
                .and()  // 拼接 条件
                .authorizeRequests() // 设置需要认证的请求地址
                .antMatchers("/","/login.html","/user/login").permitAll() // 设置 不需要认证的请求
                .anyRequest().authenticated() // 任何请求都需要认证
                .and()
                .csrf().disable(); // 关闭 security 的 csrf防护
    
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 设置认证逻辑为用户自定义认证逻辑
             * 设置密码加密处理器为 BCryptPasswordEncoder
             */
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        }
    }
    
    
  • 启动测试

    访问地址: http://127.0.0.1:9999/test/hello

    image-20210120142355279

    从上图可知,已经跳转到自定义登录界面,输入用户名和密码即可访问,如下

    image-20210120144802243

    至此完成了自定义登录界面

2.结果

2.1 实现

  • 简介

    从上述案例可知,当访问/test/hello,会跳转到登录界面,登录后在跳转到/test/hello地址上。

    但是当直接访问登录界面进行登录时,登录后会直接跳转到/请求,如下:

    image-20210120145212543

    同时当登录失败时,页面还是跳转到登录界面,只不过是地址发生了一点点变换,如下

    image-20210120145336138

    但实际开发过程中,更加希望不管是登录成功还是登录失败,都跳转到开发人员指定的地址或者处理器进行逻辑处理,接下来就来解决这个问题

  • 准备工作

    1. 复制spring-security-login-page项目,修改名字为spring-security-login-result

    2. 修改pom.xml,修改部分如下图

      image-20210120150557883
    3. 删除.impl文件,让其重新生成

    4. 将项目设置为maven项目

    5. 修改启动类名为SpringSecurityLoginResultApplication

  • 具体实现

    解决上述问题有两种方式:

    • 通过配置登录成功或者失败后的地址处理
    • 通过配置登录成功或者失败后的处理器处理

    下面就对每种方案进行实现


    自定义地址

    1. web层增加ResultController,内容如下:

      package com.briup.security.web;
      
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      @RequestMapping("/result")
      public class ResultController {
      
          /**
           * 登录成功跳转地址
           * @return
           */
          @GetMapping("/success")
          public String loginSuccess() {
              return  "登录成功";
          }
      
          /**
           * 登录失败跳转地址
           * @return
           */
          @GetMapping("/fail")
          public String loginFail() {
              return  "登录失败";
          }
      }
      
      
    2. 修改配置,内容如下:

      package com.briup.security.config;
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.password.PasswordEncoder;
      
      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
      
          @Autowired
          @Qualifier("myDetailService")
          private UserDetailsService userDetailsService;
      
          @Autowired
          private PasswordEncoder passwordEncoder;
      
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http.formLogin() // 表示使用表单进行登录
                  .loginPage("/login.html") // 表示登录界面地址
                  .loginProcessingUrl("/user/login") //表单登录地址
                  .successForwardUrl("/result/success") // 登录成功跳转的地址
                  .failureForwardUrl("/result/fail")  // 登录失败跳转的地址
                  .and()  // 拼接 条件
                  .authorizeRequests() // 设置需要认证的请求地址
                  .antMatchers("/","/login.html","/user/login").permitAll() // 设置 不需要认证的请求
                  .anyRequest().authenticated() // 任何请求都需要认证
                  .and()
                  .csrf().disable(); // 关闭 security 的 csrf防护
      
          }
      
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              /**
               * 设置认证逻辑为用户自定义认证逻辑
               * 设置密码加密处理器为 BCryptPasswordEncoder
               */
              auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
          }
      }
      
      image-20210120152921249
    3. 启动测试

      当输入正确用户名密码时,效果如下:

      image-20210120153018296

      当输入错误的用户名密码,效果如下:


      image-20210120153047087

自定义处理器

  1. 新建登录成功处理器,内容如下:

    package com.briup.security.handler;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.Collection;
    
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    
        /**
         * @param request   request对象
         * @param response  响应对象
         * @param authentication 身份认证对象,通过该对象可以获取用户名
         *
         *
         */
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            // 获取用户权限列表
            Collection authorities = authentication.getAuthorities();
            authorities.forEach(System.out::println);
    
            // 获取用户名
            String name = authentication.getName();
            System.out.println(name);
    
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.print("登录成功");
            writer.close();
        }
    }
    
    

    成功处理器必须要实现AuthenticationSuccessHandler

  2. 新建登录失败处理器,内容如下:

    package com.briup.security.handler;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    public class LoginFailHandler implements AuthenticationFailureHandler {
    
        /**
         * @param request 请求对象
         * @param response 响应对象
         * @param exception 校验的异常对象
         */
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.print("登录失败:" + exception.getMessage());
            writer.close();
        }
    }
    
    

    失败处理器必须要实现AuthenticationFailureHandler

  3. 修改配置类,内容如下

    package com.briup.security.config;
    
    import com.briup.security.handler.LoginFailHandler;
    import com.briup.security.handler.LoginSuccessHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("myDetailService")
        private UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin() // 表示使用表单进行登录
                .loginPage("/login.html") // 表示登录界面地址
                .loginProcessingUrl("/user/login") //表单登录地址
                //.successForwardUrl("/result/success") // 登录成功跳转的地址
                //.failureForwardUrl("/result/fail")  // 登录失败跳转的地址
                .successHandler(new LoginSuccessHandler())
                .failureHandler(new LoginFailHandler())
                .and()  // 拼接 条件
                .authorizeRequests() // 设置需要认证的请求地址
                .antMatchers("/","/login.html","/user/login").permitAll() // 设置 不需要认证的请求
                .anyRequest().authenticated() // 任何请求都需要认证
                .and()
                .csrf().disable(); // 关闭 security 的 csrf防护
    
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 设置认证逻辑为用户自定义认证逻辑
             * 设置密码加密处理器为 BCryptPasswordEncoder
             */
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        }
    }
    
    

    与之前的配置类相比,修改了如下图中内容

    image-20210120155256684
    1. 启动测试

      输入正确的用户名密码

      [图片上传失败...(image-c8f7a0-1611217444363)]

      同时控制台输出内容如下

      image-20210120155946228
  当输入错误用户名和密码时,如下:

  ![image-20210120160032809](https://upload-images.jianshu.io/upload_images/18110702-2ee34bf6afb24d1e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

2.2 对比

  • 获取权限方面

    通过地址进行登录的处理,无法获取到到权限

    通过处理器进行登录处理,可以获取到用户名

  • 获取用户名方面

    通过地址进行登录处理,直接在方法上注入用户名密码即可

    image-20210120163054617

    通过处理器进行登录处理,则需要借助于Authentication实例

3.分离

  • 简介

    在前后端分离情况下,登录逻辑与之前的登录逻辑不通,具体如下:

    • 后端不需要提供界面,而是提供一个登录接口,登录成功以后产生一个token,返回给前端
    • 前端在之后的请求将产生的token,发送给后端,后端进行校验,校验通过认为登录通过,让其访问具体的资源

    具体如下图所示:

    image-20210120170827558
  • 实现

    实现基于Spring Security的前后端分离登录需要以下几个步骤

    • 项目准备
    • 增加swagger
    • 增加jwt
    • 增加自定义响应结构
    • 增加处理逻辑
  • 增加校验逻辑

接下来就挨个实现上述步骤


3.1 项目准备

  1. 复制spring-security-config-account项目,修改名字为spring-security-separate-login

  2. 修改pom.xml,修改后变换的内容如下标注

    image-20210120171635669

    同时删除标签中的内容

  3. 删除.impl文件,让其重新生成

  4. 将复制的内容是其称为一个maven项目

  5. 将项目clean

  6. 修改启动类名为SpringSecuritySeparateLoginApplication

3.2 接口文档

swagger 作用这里不再解释,可以参考其他资料了解swagger的作用

修改pom.xml,增加swagger配置


  com.spring4all
  swagger-spring-boot-starter
  1.9.0.RELEASE



  org.springframework.boot
  spring-boot-starter-validation

修改配置文件application.yml,增加swagger配置

server:
  port: 9999
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://172.16.0.154:3306/test?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root
  jpa:
    show-sql: true
swagger:
  base-package: com.briup.security.web

在启动类上加上@EnableSwagger2Doc注解

image-20210121144725899

3.3 JWT配置

jwt 作用这里不再解释,可以参考其他资料了解jwt的作用

修改pom.xml,增加jwt依赖

  
    com.auth0
      java-jwt
    3.11.0
  

增加jwt工具类,内容如下

package com.briup.security.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* @author wangzh
*/
public class JwtUtil {

  /**
   * 过期时间 单位:毫秒
   */
  private static final long EXPIRE_TIME =   30 * 60 * 1000;

  private static final String SECRET = "security_jwt";

  public static final String TOKEN_HEAD = "TOKEN";

  private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);

  /**
   * 签发 token
   * @param userId 用户信息
   * @param info 用户自定义信息
   * @return
   */
  public static String sign(String userId, Map info) {
      // 设置过期时间
      Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
      // 设置加密算法
      Algorithm hmac512 = Algorithm.HMAC512(SECRET);
      return JWT.create()
                  .withAudience(userId) // 将用户id放入到token中
                  .withClaim("info",info) // 自定义用户信息
                  .withExpiresAt(date) // 设置过期时间
                  .sign(hmac512);

  }

  /**
   * 从token中获取userId
   * @param token
   * @return
   */
  public static String getUserId(String token) {
      try {
          return JWT.decode(token).getAudience().get(0);
      } catch (JWTDecodeException e) {
          logger.error(e.getMessage());
          return null;
      }
  }

  /**
   * 从token中获取自定义信息
   * @param token
   * @return
   */
  public static Map getInfo(String token) {
      try {
          Claim claim = JWT.decode(token).getClaim("info");
          return claim.asMap();
      } catch (JWTDecodeException e) {
          logger.error(e.getMessage());
          return null;
      }
  }

  /**
   * 校验token
   * @param token
   * @return
   */
  public static boolean checkSign(String token) {
      try {
          Algorithm algorithm = Algorithm.HMAC512(SECRET);
          JWTVerifier verifier = JWT.require(algorithm).build();
          verifier.verify(token);
          return true;
      } catch (Exception e) {
          logger.info("token 无效:" + e.getMessage());
          throw new RuntimeException("token无效,请重新获取");
      }

  }



}

3.4 响应结构

前后端分离中,增加自定义响应结构,这样可以更加规范后端返回给前端的数据样式

修改pom.xml,增加lombok依赖


    org.projectlombok
    lombok

增加自定义响应结构类

package com.briup.security.util;

import lombok.Data;
import lombok.Getter;

@Getter
public class Result {
    /**
     * 业务状态码
     */
    private Integer code;
    /**
     * 状态码信息对应数据
     */
    private String message;

    /**
     * 响应时间
     */
    private Long time;

    /**
     * 响应数据
     */
    private T data;

    private Result(Integer code,String message,T data) {
        this.code = code;
        this.message = message;
        this.time = System.currentTimeMillis();
        this.data = data;
    }

    public static  Result success(E data) {
        return new Result<>(200,"成功",data);
    }


    public static  Result success() {
        return success(null);
    }

    public static  Result fail(Integer code,String message,E data) {
        return new Result<>(code,message,data);
    }

   public static Result fail(Integer code,String message) {
       return new Result<>(code, message, null);
   }
}

3.5 结果处理

这里演示在处理器 和 url返回token

3.5.1 处理器处理

  1. 登录成功以后,在处理器返回token

    增加登录controller

    package com.briup.security.web;
    
    import io.swagger.annotations.Api;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @Api(tags = "账户管理")
    @RequestMapping("/account")
    public class AccountController {
    
        @PostMapping("/login")
        public void login(String username,String password) {
        }
    }
    
    

    注意:这里不要写任何逻辑,spring security自己会去校验用户名和密码

    新增成功处理器,内容如下

    package com.briup.security.handler;
    
    import com.briup.security.util.JwtUtil;
    import com.briup.security.util.Result;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    public class SuccessHandler implements AuthenticationSuccessHandler {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            // 登录成功产生token
            String name = authentication.getName();
            String token = JwtUtil.sign(name, null);
            String result = objectMapper.writeValueAsString(Result.success(token));
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(result);
            writer.close();
        }
    }
    

    新增失败处理器

    package com.briup.security.handler;
    
    import com.briup.security.util.Result;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    public class FailHandler implements AuthenticationFailureHandler {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            String result = objectMapper.writeValueAsString(Result.fail(501, "用户名密码错误"));
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(result);
            writer.close();
        }
    }
    
    

    修改配置类,内容如下:

    package com.briup.security.config;
    
    import com.briup.security.handler.FailHandler;
    import com.briup.security.handler.SuccessHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("myDetailService")
        private UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Bean
        public SuccessHandler successHandler() {
            return new SuccessHandler();
        }
    
        @Bean
        public FailHandler failHandler() {
            return new FailHandler();
        }
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()
                .loginPage("/account/page") // 当请求需要认证,跳转到该地址
                .loginProcessingUrl("/account/login") // 请求认证地址
                .successHandler(successHandler())
                .failureHandler(failHandler())
                .and()
                .authorizeRequests()
                .antMatchers("/account/login","/account/page").permitAll() // 登录请求也不需要认证
                .antMatchers(
                            "/webjars/**",
                            "/api/**",
                            "/swagger-ui.html",
                            "/swagger-resources/**",
                            "/v2/**",
                            "/swagger-resources/**").permitAll() // swagger 界面不需要认证
                .anyRequest().authenticated()
                .and().csrf().disable();
    
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 设置认证逻辑为用户自定义认证逻辑
             * 设置密码加密处理器为 BCryptPasswordEncoder
             */
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        }
    }
    

    启动测试

    image-20210121145252135

    访问登录请求,可以看到返回token地址

    image-20210121145342810

    同时用户名输入错误,也可以看到对应的结果

    image-20210121145414947

    访问其他请求,发现也访问不了

    image-20210121145501945

    至此,完成了登录成功后在处理器返回token

3.5.2 URL处理

登录成功后通过url返回token

增加登录成功或者失败以后的接口

package com.briup.security.web;

import com.briup.security.util.JwtUtil;
import com.briup.security.util.Result;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "账户管理")
@RequestMapping("/account")
public class AccountController {

    @PostMapping("/login")
    public void login(String username,String password) {
    }

    @GetMapping("/page")
    public Result page() {
        return Result.fail(401,"该请求需要认证");
    }

    @PostMapping("/success")
    public Result success(String username) {
        return Result.success(JwtUtil.sign(username,null));
    }


    @PostMapping("/fail")
    public Result fail(String username) {
        return Result.fail(401,"用户名密码错误");
    }
}

修改配置类,内容如下

package com.briup.security.config;

import com.briup.security.handler.FailHandler;
import com.briup.security.handler.SuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("myDetailService")
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public SuccessHandler successHandler() {
        return new SuccessHandler();
    }

    @Bean
    public FailHandler failHandler() {
        return new FailHandler();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .loginPage("/account/page") // 当请求需要认证,跳转到该地址
            .loginProcessingUrl("/account/login") // 请求认证地址
           // .successHandler(successHandler())
           // .failureHandler(failHandler())
            .successForwardUrl("/account/success")
            .failureForwardUrl("/account/fail")
            .and()
            .authorizeRequests()
            .antMatchers("/account/login","/account/page").permitAll() // 登录请求也不需要认证
            .antMatchers(
                        "/webjars/**",
                        "/api/**",
                        "/swagger-ui.html",
                        "/swagger-resources/**",
                        "/v2/**",
                        "/swagger-resources/**").permitAll() // swagger 界面不需要认证
            .anyRequest().authenticated()
            .and().csrf().disable();

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         * 设置认证逻辑为用户自定义认证逻辑
         * 设置密码加密处理器为 BCryptPasswordEncoder
         */
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
}

效果与之前效果一样,这里就不做演示了

3.6 TOKEN校验

认证完成,则需要校验token,校验token其原理很简单,具体如下:

  • UsernamePasswordAuthenticationFilter过滤器执行之前增加一个自定义过滤器
  • 自定义过滤器就是用来校验token,token合法则将请求转发给下一个过滤器

接下来就来实现上述过程,具体如下

  1. 自定义过滤器

    package com.briup.security.filter;
    
    import com.briup.security.util.JwtUtil;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    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;
    
    public class AuthenticationTokenFilter extends OncePerRequestFilter {
    
        private static final Logger logger = LoggerFactory.getLogger(AuthenticationTokenFilter.class);
    
        @Autowired
        @Qualifier("myDetailService")
        private UserDetailsService userDetailsService;
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 1.从请求头中获取Token
            String token = request.getHeader(JwtUtil.TOKEN_HEAD);
    
            //2.判断token是否为空,则请求放心,让UsernamePasswordAuthenticationFilter校验用户名密码
            if (token == null || "".equals(token)) {
                filterChain.doFilter(request,response);
                return;
            }
    
            try {
                //3.如果token不为空,则去校验token,
                if (JwtUtil.checkSign(token)) {
                    // 获取用户信息
                    String userId = JwtUtil.getUserId(token);
                    UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
                    /**
                     *  UsernamePasswordAuthenticationToken
                     *      这个对象使用来保存用户信息
                     *  如果SecurityContextHolder.getContext()中有该对象,那么就不需要再次校验
                     */
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            } catch (Exception e) {
                e.printStackTrace();
                logger.info("校验用户名密码失败");
            }
        }
    }
    
  2. 修改配置类,将上述过滤器添加到UsernamePasswordAuthenticationFilter前面

    package com.briup.security.config;
    
    import com.briup.security.filter.AuthenticationTokenFilter;
    import com.briup.security.handler.FailHandler;
    import com.briup.security.handler.SuccessHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("myDetailService")
        private UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Bean
        public SuccessHandler successHandler() {
            return new SuccessHandler();
        }
    
        @Bean
        public FailHandler failHandler() {
            return new FailHandler();
        }
    
    
        @Bean
        public AuthenticationTokenFilter authenticationTokenFilter() {
            return new AuthenticationTokenFilter();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 添加过滤器到 UsernamePasswordAuthenticationFilter前面
            
            http.formLogin()
                .loginPage("/account/page") // 当请求需要认证,跳转到该地址
                .loginProcessingUrl("/account/login") // 请求认证地址
                .successHandler(successHandler())
                .failureHandler(failHandler())
               // .successForwardUrl("/account/success")
               // .failureForwardUrl("/account/fail")
                .and()
                .authorizeRequests()
                .antMatchers("/account/login","/account/page").permitAll() // 登录请求也不需要认证
                .antMatchers(
                            "/webjars/**",
                            "/api/**",
                            "/swagger-ui.html",
                            "/swagger-resources/**",
                            "/v2/**",
                            "/swagger-resources/**").permitAll() // swagger 界面不需要认证
                .anyRequest().authenticated()
                .and().csrf().disable();
    
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 设置认证逻辑为用户自定义认证逻辑
             * 设置密码加密处理器为 BCryptPasswordEncoder
             */
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        }
    }
    
    
  3. 启动测试

    进行登录产生token

    image-20210121160944699

    将token添加到swagger认证中

    image-20210121161013058

    访问test/hello请求

    image-20210121161036582

    说明过滤器生效,且校验通过

4.地址

代码地址:https://gitee.com/wangzh991122/security.git

你可能感兴趣的:(spring security(二)-自定义认证(包含前后端分离认证))