Bootstrap

Spring security配置

SpringSecurity认证

一、HttpBasic模式登录认证

SpringSecurity 自带一种基础认证模式

实现方式:创建WebSecurityConfig配置类

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       super.configure(auth);
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       super.configure(web);
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //开启httpBasci模式登录认证
       http.httpBasic()
               //每个模块配置使用and结尾
               .and()
               //配置路径拦截,表明路径访问所对应的权限,角色,认证信息
               .authorizeRequests()
               .anyRequest()
               //所有请求都需要登录认证才能访问
               .authenticated();
   }
}
二、 默认formLogin表单模式

注释掉上面的配置类,通过applicaton.yml配置用户名与密码

spring:
   security:
           user:
               name: tom
               password: tom
三、 自定义formLogin表单模式

继承WebSecurityConfigurerAdapter类,实现它的三个configure方法

//@Configuration
@EnableWebSecurity //涵盖了 @Configuration 注解
//@EnableGlobalMethodSecurity(jsr250Enabled = true)
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启Security注解鉴权的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private PasswordEncoder passwordEncoder;
   @Autowired
   private UserServiceImpl userServiceImpl;
   @Autowired
   private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
   @Autowired
   private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
   @Autowired
   private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
   @Autowired
   private MyAccessDeniedHandler myAccessDeniedHandler;
   @Autowired
   private JwtTokenAuthenticationFilter jwtTokenAuthenticationFilter;
   @Autowired
   private MyLogoutSuccessHandler myLogoutSuccessHandler;
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       //在内存中配置用户名和密码
//        auth.inMemoryAuthentication().withUser("tom")
//                .password(passwordEncoder.encode("123")).roles();
//        auth.inMemoryAuthentication().withUser("admin")
//                .password(passwordEncoder.encode("admin")).roles();
       //从指定的数据库读取账号密码
//        auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
       //指定认证管理器
       auth.authenticationProvider(getDaoAuthenticationProvider());
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       super.configure(web);
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //开启httpBasic认证
//        http.httpBasic()
//                //每个模块配置使用and结尾
//                .and()
//                //配置路径拦截,表明路径访问所对应的权限,角色,认证信息
//                .authorizeRequests()
//                .anyRequest()
//                //所有请求都需要登录认证才能访问
//                .authenticated();
       //http.httpBasic().and().authorizeRequests().anyRequest().authenticated(); //关闭httpBasic认证

       //开启自定义formLogin表单认证
       //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
       http.authorizeRequests().antMatchers("/login", "/login.html")
               .permitAll().anyRequest().authenticated().and().
               // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
                       formLogin().loginPage("/login.html").loginProcessingUrl("/login")
               //登录表单form中密码输入框input的name名,不修改的话默认是password
               .usernameParameter("username").passwordParameter("password")
               //登录认证成功后默认转跳的路径
//                .defaultSuccessUrl("/home")
               //登陆成功后不跳转,返回json数据
               .successHandler(myAuthenticationSuccessHandler)
               //登录认证失败后,被放行的请求
               //注意,此请求不能为error,因为已经被springSecurity定义
//                .failureUrl("/gotoError1").permitAll();
               //登录认证失败后,不跳转,返回json数据
               .failureHandler(myAuthenticationFailureHandler)
               .and()
               .exceptionHandling()
               .authenticationEntryPoint(myAuthenticationEntryPoint)
               .accessDeniedHandler(myAccessDeniedHandler)
                       .and().logout().logoutSuccessHandler(myLogoutSuccessHandler);
       //将自定义的JwtTokenAuthenticationFilter插入到过滤器链的指定过滤器前面
       http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
       //关闭CSRF跨域
       http.csrf().disable();
       //关闭session最严格的策略
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
   }

   /**
    * 重新定制DaoAuthenticationProvider
    * @return
    */
   @Bean
   public DaoAuthenticationProvider getDaoAuthenticationProvider(){
       DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
       //设置用户未找到的异常不隐藏
       provider.setHideUserNotFoundExceptions(false);
       //设置认证管理器使用userServiceImpl对象
       provider.setUserDetailsService(userServiceImpl);
       //设置认证管理器使用的密码管理对象
       provider.setPasswordEncoder(passwordEncoder);
       return provider;
   }
}
1. 配置需要直接放行的url,处理认证请求的路径
protected void configure(HttpSecurity http) throws Exception {
   //开启自定义表单模式登录认证
   //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
   http.authorizeRequests().antMatchers("/login", "/login.html")
       .permitAll().anyRequest().authenticated()
       .and().
       // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
       formLogin().loginPage("/login.html").loginProcessingUrl("/login")
       //登录表单form中密码输入框input的name名,不修改的话默认是password
       .usernameParameter("username").passwordParameter("password")
       //登录认证成功后默认转跳的路径
       .defaultSuccessUrl("/home");
   //关闭CSRF跨域攻击防御
   http.csrf().disable();
}
2. 配置PasswordEncoder密码加密

步骤1: 首先在启动类上创建 Bean注解方法

@SpringBootApplication
public class SpringbootSecurityDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(SpringbootSecurityDemoApplication.class, args);
   }

   //创建一个PasswordEncoder加密器存入容器中
   @Bean
   public PasswordEncoder getPasswordEncoder(){
       return new BCryptPasswordEncoder();
   }
}

步骤2: 在测试类中测试PasswordEncoder方法

@SpringBootTest
class SpringbootSecurityDemoApplicationTests {
   @Autowired
   private PasswordEncoder passwordEncoder;

   @Test
   void contextLoads() {
       String pwd1 = passwordEncoder.encode("123");
       String pwd2 = passwordEncoder.encode("123");
       System.out.println(pwd1); //$2a$10$21Ae7HsPihdZNu.YWhqeHu4dJ5/45l5mpQxN8P3HWzPNlDeh84cm2
       System.out.println(pwd2); //$2a$10$000Misx3wpASVQkzz.iyK.q9L9qx7Thl7mey/UzoLEAvVJgenvGc.
       System.out.println(passwordEncoder.matches("123",pwd1));
       System.out.println(passwordEncoder.matches("123",pwd2));
       //$2a$10$s65QPon1AZTjZhQjlL.jF.8JBeIjdPLoL.UfZkDA5Uzv94yQhq.RG
       //$2a$10$/Rs7Zr41nC6UWgP/Ij0tnOnrBwMLjwRDYrGJrNMzMMlWQB5XgmBJe
   }
}

步骤3: 在配置类中使用加密的方式在内存中配置用户名和密码,修改类WebSecurityConfig

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private PasswordEncoder passwordEncoder;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.inMemoryAuthentication().withUser("tom")
               .password(passwordEncoder.encode("123")).roles();
       auth.inMemoryAuthentication().withUser("admin")
               .password(passwordEncoder.encode("admin")).roles();
   }
   
   ...
}
3. 配置动态认证

即从自定义的数据库中查询数据进行用户登录,而不是从内存中

步骤1:编写UserServiceImpl实现UserDetailsService接口

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
       implements UserService, UserDetailsService {
   @Autowired
   private UserMapper userMapper;
   /**
    * 根据用户名查询用户UserDetails
    * @param username
    * @return
    * @throws UsernameNotFoundException
    */
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       //访问数据库,根据用户查询用户对象
       QueryWrapper<User>  wrapper = new QueryWrapper<>();
       wrapper.eq("username",username);
       User user = userMapper.selectOne(wrapper);
       if(user==null){
           throw new UsernameNotFoundException("用户名不存在!");
       }
       //封装用户的权限集
       List<GrantedAuthority> authorities = new ArrayList<>();

       //封装数据库存询的用户信息
       UserDetails userDetails =
               new org.springframework.security.core.userdetails.User(
                       user.getUsername(),user.getPassword(),authorities);

       return userDetails;
   }
}

步骤2:添加配置类相关代码

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private PasswordEncoder passwordEncoder;

   @Autowired
   private UserServiceImpl userServiceImpl;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
   }
}
4. 配置异常处理

步骤1: 在配置类中配置异常的处理的url

 @Override
   protected void configure(HttpSecurity http) throws Exception {
       //开启自定义表单模式登录认证
       //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
       http.authorizeRequests().antMatchers("/login", "/login.html")
               .permitAll().anyRequest().authenticated()
               .and().
               // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
                       formLogin().loginPage("/login.html").loginProcessingUrl("/login")
               //登录表单form中密码输入框input的name名,不修改的话默认是password
               .usernameParameter("username").passwordParameter("password")
               //登录认证成功后默认转跳的路径
               .defaultSuccessUrl("/home")
               //登录认证失败后请求URL ,要放行
               .failureUrl("/error1").permitAll();
       //关闭CSRF跨域攻击防御
       http.csrf().disable();
   }

注意:此处不能指定/error ,因为这个 /error是security内置的请求处理

步骤2: 编写erro1处理器

@GetMapping("/error1")
public String error(HttpServletRequest request, HttpServletResponse response){
   return "error";
}

步骤3: 创建error.html模板页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
出错了: <p th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></p>
</body>
</html>

附加步骤4: 用户名找不到异常处理

由于spring security框架底层默认将UsernameNotFoundException设置为隐藏,而显示的是BadCredential异常,可以通过下面的方式配置实现

/**
* Spring Securtiy配置类
*/
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private PasswordEncoder passwordEncoder;

   @Autowired
   private UserServiceImpl userServiceImpl;

   //重新定制DaoAuthenticationProvider
   @Bean
   public DaoAuthenticationProvider getDaoAuthenticationProvider(){
       DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
       //设置用户未找到异常不隐藏
       provider.setHideUserNotFoundExceptions(false);
       //设置认证管理器使用UserDetaisService对象--此处使用的是我们自定义的业务实现类
       provider.setUserDetailsService(userServiceImpl);
       //设置认证管理器使用的密码检验器对象
       provider.setPasswordEncoder(passwordEncoder);
       return provider;
   }

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       //指定认证管理器
       auth.authenticationProvider(getDaoAuthenticationProvider());
   }
   
   ...
}
5. 配置前后端分离认证成败的处理
5-1 认证成功的处理

登录成功之后我们是跳转到/home控制器的,也就是跳转到home.html

但是在前后端分离的情况下,页面的跳转是交给前端去控制的,后端的控制器就不生效了,那我们应该如何实现让前端去跳转页面呢?

我们发现在认证成功后,执行的是AuthenticationSuccessHandler接口实现类:

默认为SimpleUrlAuthenticationSuccessHandler,如果定制自定义处理器,只需要实现该接口

/**
* 自定义认证成功的处理器Handler
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                       Authentication authentication) throws IOException, ServletException {
       ResponseResult<Void> result = ResponseResult.ok();
       response.setContentType("application/json;charset=utf-8");
       PrintWriter out= response.getWriter();
       out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
       out.flush();
       out.close();
   }
}

然后配置成功的处理器:

@Override
protected void configure(HttpSecurity http) throws Exception {
   //开启自定义表单模式登录认证
   //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
   http.authorizeRequests().antMatchers("/login", "/login.html")
       .permitAll().anyRequest().authenticated()
       .and().
       // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
       formLogin().loginPage("/login.html").loginProcessingUrl("/login")
       //登录表单form中密码输入框input的name名,不修改的话默认是password
       .usernameParameter("username").passwordParameter("password")
       //登录认证成功后默认转跳的路径
       //.defaultSuccessUrl("/home")
       // 前后端分离认证成功的处理器 -输出json
       .successHandler(myAuthenticationSuccessHandler)
       .failureUrl("/error1").permitAll();
   //关闭CSRF跨域攻击防御
   http.csrf().disable();
}
5-2 认证失败的处理

同样的,有登录成功的处理器就有登录失败的处理器,但是登录失败的情况比较多,所以需要经过很多的判断,登录失败处理器主要用来对登录失败的场景(密码错误、账号锁定等…)做统一处理并返回给前台统一的json返回体

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

   public void onAuthenticationFailure(HttpServletRequest request,
                                       HttpServletResponse response, AuthenticationException e)
                                       throws IOException, ServletException {
       //定义响应的结果对象
       ResponseResult<String> result = null;
       if(e instanceof UsernameNotFoundException){
           result = ResponseResult.error(ResultCode.USER_ACCOUNT_NOT_EXIST);
       }else if (e instanceof AccountExpiredException) {
           //账号过期
           result = ResponseResult.error(ResultCode.USER_ACCOUNT_EXPIRED);
       } else if (e instanceof BadCredentialsException) {
           //凭证不对   错误
           result = ResponseResult.error(ResultCode.USER_CREDENTIALS_ERROR);
       } else if (e instanceof CredentialsExpiredException) {
           //密码过期
           result = ResponseResult.error(ResultCode.USER_CREDENTIALS_EXPIRED);
       } else if (e instanceof DisabledException) {
           //账号不可用
           result = ResponseResult.error(ResultCode.USER_ACCOUNT_DISABLE);
       } else if (e instanceof LockedException) {
           //账号锁定
           result = ResponseResult.error(ResultCode.USER_ACCOUNT_LOCKED);
       } else{
           result = ResponseResult.error(ResultCode.COMMON_FAIL);
       }

       response.setContentType("application/json;charset=UTF-8");
       response.setHeader("Access-Control-Allow-Origin", "*");
       response.setHeader("Access-Control-Allow-Method", "POST,GET");

       response.setContentType("application/json;charset=utf-8");
       PrintWriter out= response.getWriter();
       out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
       out.flush();
       out.close();
   }
}

添加配置:

 @Override
   protected void configure(HttpSecurity http) throws Exception {
             ....
               .successHandler(myAuthenticationSuccessHandler)
               // 前后端分离认证失败的处理器 -输出json
               .failureHandler(myAuthenticationFailureHandler);
               
       //关闭CSRF跨域攻击防御
       http.csrf().disable();
   }
6. 配置前后端分离用户未登录的处理

而在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽重定向的登录页面,并返回统一的json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)

//自定义用户未登录的处理器
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
   //重新定义未登录的处理
   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
       ResponseResult<Void> result = ResponseResult.error(ResultCode.USER_NOT_LOGIN);
       response.setContentType("application/json;charset=utf-8");
       PrintWriter out= response.getWriter();
       out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
       out.flush();
       out.close();
   }
}

配置用户未登录的处理

@Override
public void configure(WebSecurity web) throws Exception {
   super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
   ...

       // 前后端分离认证成功的处理器 -输出json
       .successHandler(myAuthenticationSuccessHandler)
       // 前后端分离认证失败的处理器 -输出json
       .failureHandler(myAuthenticationFailureHandler)
       .and()
       // 前后端分离处理未登录请求
       .exceptionHandling()
       .authenticationEntryPoint(myAuthenticationEntryPoint);
   //关闭CSRF跨域攻击防御
   http.csrf().disable();
}
7. 鉴权

步骤1:在配置类上添加注解配置

@EnableWebSecurity
//@EnableGlobalMethodSecurity(jsr250Enabled = true)  //开启Security注解鉴权
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)  //sprintSecurity自带 可以支持Spring EL表达式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   ...
}

步骤2:在控制器方法上使用注解,表示必须拥有该注解标识的权限才能访问

@RestController
public class UserController {
   @Autowired
   private UserService userService;

   @RequestMapping("/userList")
   //一旦使用此注解,表示请求该方法的用户权限集里必须该权限标识符
   //@RolesAllowed("ROLE_teacher:list")  //访问到数据库表中的权限标识符必须以ROLE_开头,注解上的ROLE_可以省略
   //@Secured("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头 注解上的ROLE_不能省略
   //@PreAuthorize("hasAnyAuthority('teacher:list')") //使用hashAnyAuthority EL表达式,可以指定权限标识,不要求使用ROL_ 开头
   //@PreAuthorize("hasAnyRole('ROLE_teacher:list')") //使用hashAnyROLE EL表达式,可以指定权限标识,要求使用ROL_ 开头 ,数据库表中的权限标识也必须以ROLE开头
   @PreAuthorize("hasRole('ROLE_teacher:list')")
   public List<User> queryUserList(){
       return userService.list(null);
   }
}

步骤3:权限不足的处理方案

/**
* 权限不足的处理
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
       ResponseResult<Void> result = ResponseResult.error(ResultCode.NO_PERMISSION);
       response.setContentType("application/json;charset=utf-8");
       PrintWriter out= response.getWriter();
       out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
       out.flush();
       out.close();
   }
}

步骤4:配置类

http.authorizeRequests().antMatchers("/login", "/login.html")
   .permitAll().anyRequest().authenticated()
   .and().
   // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
   formLogin().loginPage("/login.html").loginProcessingUrl("/login")
   //登录表单form中密码输入框input的name名,不修改的话默认是password
   .usernameParameter("username").passwordParameter("password")
   //登录认证成功后默认转跳的路径
   //.defaultSuccessUrl("/home")
   // 前后端分离认证成功的处理器 -输出json
   .successHandler(myAuthenticationSuccessHandler)
   // 前后端分离认证失败的处理器 -输出json
   .failureHandler(myAuthenticationFailureHandler)
   .and()
   // 前后端分离处理未登录请求
   .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
   // 前后端分离处理权限不足的请求
   .accessDeniedHandler(myAccessDeniedHandler);
//.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
8. 整合JWT

步骤1:添加依赖jar

<!--用于生成JWT依赖-->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>

<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>3.8.1</version>
</dependency>

步骤2:编写工具类JwtTokenUtil并测试

public class JwtTokenUtil {
   /**
    * 过期时间50分钟
    */
   private static final long EXPIRE_TIME = 5 * 60 * 10000;
   /**
    * jwt 密钥
    */
   private static final String SECRET = "woniuxy";

   /*
      生成签名  50分钟过期
    */
   public static String createSign(String userName) throws Exception {
       try {
           Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
           Algorithm algorithm = Algorithm.HMAC256(SECRET);
           return JWT.create()
                   // 将 user id 保存到 token 里面
                   .withAudience(userName)
                   // 50分钟后token过期
                   .withExpiresAt(date)
                   //.withClaim()
                   //.withSubject(userName)
                   // token 的密钥
                   .sign(algorithm);
       }catch(Exception ex){
           ex.printStackTrace();
           throw new Exception("签名错误");
       }
   }

   /**
    * 根据token获取username
    * @param token
    * @return
    */
   public static String getUserId(String token) {
       try {
           String userId = JWT.decode(token).getAudience().get(0);
           return userId;
       } catch (JWTDecodeException e) {
           throw new JWTDecodeException("生成的token 异常");
       }
   }

   /**
    * 校验token 是否有效
    * @param token
    * @return
    */
   public static boolean checkSign(String token) {
       try {
           Algorithm algorithm = Algorithm.HMAC256(SECRET);
           JWTVerifier verifier = JWT.require(algorithm)
                   // .withClaim("username", username)
                   .build();
           DecodedJWT jwt = verifier.verify(token);
           return true;
       } catch (JWTVerificationException exception) {
           throw new RuntimeException("token 无效,请重新获取");
       }
   }

   public static void main(String[] args) throws Exception {
       //测试生成Token串
       String strToken = JwtTokenUtil.createSign("zhangsan");
       System.out.println(strToken);

       //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo
       //验证 token是否有效
       boolean isValid = JwtTokenUtil.checkSign("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
       System.out.println(isValid);

       //从给定的token串获取用户信息
       String username = JwtTokenUtil.getUserId("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
       System.out.println(username);
   }
}

步骤3:自定义认证成功的处理器Handler,用于登录认证成功后生成token并返回

**
* 自定义认证成功的处理器Handler
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       try {
           //获取当前登录认证成功的用户名
           String username = request.getParameter("username");
           String strToken = JwtTokenUtil.createSign(username);

           //通过响应的json返回客户端
           ResponseResult<String> result = new ResponseResult<>(strToken,"OK",200);
           response.setContentType("application/json;charset=utf-8");
           PrintWriter out = response.getWriter();
           //将对象转json输出
           out.write(new ObjectMapper().writeValueAsString(result));
           out.flush();
           out.close();
       }catch(Exception e){
           e.printStackTrace();
       }
   }
}

第2步,携带Token发送请求

要想使FilterSecurityInterceptor过滤器放行:

1. Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且应该是已认证状态。
2. Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。

实现思路:

关键在于:在 FilterSecurityInterceptor 之前 要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。

基于上述思路,

步骤4:我们要自定义实现一个 Filter :

/**
*  将用户请求中携带的 JWT 转化为 Authentication Token
*  存入 Spring Security 上下文( Context )
*  表示每次请求只执行该过滤器一次
*/
@Component
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
   @Autowired
   private UserMapper userMapper;

   @Autowired
   private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

   @Override
   protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) throws ServletException, IOException {
       //获取当前请求的uri
       String uri = request.getRequestURI();
       //如果是认证请求
       if(uri.endsWith("/login")){
           //放行
           filterChain.doFilter(request,response);
           return;
       }

        //不是认证请求--获取请求中的头部的token串
       String strToken = request.getHeader("strToken");
       if(StringUtils.isEmpty(strToken)){
           //抛出自定义异常 -Token为null
           myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
                   new TokenIsNullException("Token为空!"));
           return ;
       }
       //不是空,且不是认证请求
       try {
           //检验token是否有效
           if (JwtTokenUtil.checkSign(strToken)) {
               //获取token中的用户名
               String username = JwtTokenUtil.getUserId(strToken);
               //查询数据库获取用户的权限集
               List<String> percodes = userMapper.getPerCodesByPerm(username);
               List<GrantedAuthority> authorities = new ArrayList<>();
               percodes.forEach(percode->{
                   authorities.add(new SimpleGrantedAuthority(percode));
               });
               //封装数据库存询的用户信息
               UsernamePasswordAuthenticationToken  usernamePasswordAuthenticationToken
                       = new UsernamePasswordAuthenticationToken(username,"",authorities);

               //存入securityContext
               SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
               //放行
               filterChain.doFilter(request,response);
           }
       }catch(Exception e){
           ///抛出自定义异常  Token无效
           myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
                   new TokenIsInvalidException("Token无效!"));
       }
   }
}

步骤5:然后将过滤器插入到FilterChainPrxoy代理的过滤器链中的UsernamePasswordAuthencationFilter前面

//将自定义的JwtTokenAuthenticationFilter插入到过滤器链中的指定的过滤器前面
            http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
9. 注销成功处理方案

步骤1:自定义注销成功的处理器MyLogoutSuccessHandler实现LogoutSuccessHandler接口

//注销成功的处理器
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
   @Override
   public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                               Authentication authentication) throws IOException, ServletException {
       String headerToken = request.getHeader("strToken");
       System.out.println("logout header Token:"+headerToken);
       if(!StringUtils.isEmpty(headerToken)){ //如果token不是空
           SecurityContextHolder.clearContext(); //清空上下文 用户名与权限集UsernamePasswordAuthenticationToken
           ResponseResult<String> result = new ResponseResult<>("","注销成功",200);
           response.setContentType("application/json;charset=UTF-8");
           response.getWriter().write(new ObjectMapper().writeValueAsString(result));
       }else{
           ResponseResult<Void> result = ResponseResult.error(ResultCode.TOKEN_IS_NULL);
           response.setContentType("application/json;charset=utf-8");
           PrintWriter out= response.getWriter();
           out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
           out.flush();
           out.close();
       }
   }
}

步骤2:在WebSecurityConfig配置类中 配置注销成功处理器

 // 前后端分离处理注销成功操作
.and().logout().logoutSuccessHandler(myLogoutSuccessHandler);

//关闭session最严格的策略 -JWT认证的情况下,不需要security会话参与
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
;