【编程不良人】SpringSecurity实战学习笔记02---自定义认证

  • 认证配置

  • 表单认证

  • 注销登录

  • 前后端分离认证

  • 添加验证码

3.1 自定义资源权限规则

配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

  • /index:主页,公共资源,可以直接访问

  • /hello .... :受保护资源,需要权限管理(认证授权之后才能访问),默认所有资源都是受保护的,需要认证授权之后才能访问

在项目中添加如下配置就可以实现对资源权限规则设定:

 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
     @Override
     /**
      * 说明: SpringBoot2.6.2版本
      *    permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
      *    anyRequest().authenticated() 代表所有请求,必须认证之后才能访问
      *    formLogin() 代表开启表单认证
      * 注意: 放行资源必须放在所有认证请求之前!
      */
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeHttpRequests()
                 .mvcMatchers("/index").permitAll() //放行资源写在任何前面
                 .anyRequest().authenticated()
                 .and().formLogin();
     }
 }

注意:在SpringBoot 2.7.1 中的spring-security-config-5.7.2.RELEASE中已提到WebSecurityConfigurerAdapter已过时被弃用,替代方法如下:

  1. 使用 SecurityFilterChain Bean 来配置 HttpSecurity;

  2. 使用 WebSecurityCustomizer Bean 来配置 WebSecurity。

参考链接:

  • SpringSecurity - WebSecurityConfigurerAdapter 过时问题_爱上云原生的博客-CSDN博客

  • Spring Security即将弃用配置类WebSecurityConfigurerAdapter_码农小胖哥的博客-CSDN博客

替代后新代码为:

 package com.study.config;
 ​
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.web.SecurityFilterChain;
 ​
 /**
  * @ClassName WebSecurityConfig
  * @Description 自定义资源权限规则
  * @Author Jiangnan Cui
  * @Date 2022/7/6 12:23
  * @Version 1.0
  */
 @Configuration
 @EnableWebSecurity //添加security过滤器,此处一定要加入此注解,否则下面的httpSecurity无法装配
 public class WebSecurityConfig {
     /**
      * 说明: SpringBoot2.7.1版本
      *    permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
      *    anyRequest().authenticated() 代表所有请求,必须认证之后才能访问
      *    formLogin() 代表开启表单认证
      * 注意: 放行资源必须放在所有认证请求之前!
      */
     @Bean
     SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
         return httpSecurity
                 .authorizeHttpRequests()
                 .mvcMatchers("/index").permitAll()
                 .anyRequest().authenticated()
                 .and().formLogin().and().build();
     }
 }

总结:新替代方案中Spring Security 就不需要再去继承WebSecurityConfigurerAdapter,然后重写 configure()方法了,直接通过 filterChain() 方法就能使用 HttpSecurity 来配置相关信息。

重启项目,测试访问index请求时直接放行,访问hello请求时需要登录后才能正常访问!!!

3.2 自定义登录界面

配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

  • pom.xml引入Thymeleaf模板依赖

     
     
         org.springframework.boot
         spring-boot-starter-thymeleaf
     
  • application.properties配置Thymeleaf

 # Thymeleaf配置
 # 关闭缓存
 spring.thymeleaf.cache=false 
 # 默认前缀路径,可不配置
 spring.thymeleaf.prefix=classpath:/templates/
 # 默认文件后缀,可不配置
 spring.thymeleaf.suffix=.html 
  • 定义登录页面LoginController

     package com.study.controller;
     ​
     import org.springframework.stereotype.Controller;
     import org.springframework.web.bind.annotation.RequestMapping;
     ​
     /**
      * @ClassName LoginController
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/7/23 17:47
      * @Version 1.0
      */
     @Controller
     public class LoginController {
         @RequestMapping("/login.html") //注意此处是login.html而不是login
         public String login() {
             return "login";//封装为login.html
         }
     }
  • 在 templates 中定义登录界面login.html

     
     
     
         
         登录页面
     
     
     

    用户登录

     
        用户名:
        密码:
           
       

    需要注意的是:

    • 登录表单 method 必须为 post,action 的请求路径可以定义为 /doLogin

    • 用户名的 name 属性为 username,用户名可以指定为除username外的其他名称

    • 密码的 name 属性为 password,密码可以指定为除password外的其他名称

  • 配置 Spring Security 配置类WebSecurityConfigurer

     package com.study.config;
     ​
     import org.springframework.context.annotation.Configuration;
     import org.springframework.security.config.annotation.web.builders.HttpSecurity;
     import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
     ​
     /**
      * @ClassName WebSecurityConfigurer
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/7/21 22:34
      * @Version 1.0
      */
     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
                     .mvcMatchers("/index").permitAll() //放行/index请求
                     .mvcMatchers("/login.html").permitAll() //放行/login.html请求
                     .anyRequest().authenticated() //其它请求需要登录认证后才能访问
                     .and()
                     .formLogin() //默认form表单页面登录
                     .loginPage("/login.html") //使用自定义登录页面登录页面登录
                     .loginProcessingUrl("/doLogin") //使用自定义登录页面时需要重新指定url,对应login.html中的action路径
                     .usernameParameter("uname") //重新指定用户名名称
                     .passwordParameter("pwd") //重新指定密码名称
                     .successForwardUrl("/index") //认证成功后跳转路径
                     //forward 跳转路径  始终在认证成功之后跳转到指定请求 地址栏不变
                     //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径
                     //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false
                     //redirect 重定向  注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变
                     .failureUrl("/login.html") //登录失败后跳转路径
                     .and()
                     .csrf().disable();//此处先关闭CSRF跨站保护
         }
     }
     ​

    注意:successForwardUrl 、defaultSuccessUrl 这两个方法都可以实现成功之后跳转,其中:

    • successForwardUrl 默认使用 forward跳转 注意:不会跳转到之前请求路径

    • defaultSuccessUrl 默认使用 redirect 跳转 注意:如果之前请求路径,会有优先跳转之前请求路径,可以传入第二个参数进行修改

3.3 自定义登录成功处理

配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

       有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面,只需要给前端返回一个 JSON数据通知登录成功还是失败与否,这个时候可以通过自定义 AuthenticationSucccessHandler 实现。

 public interface AuthenticationSuccessHandler {
     /**
      * Called when a user has been successfully authenticated.
      * @param request the request which caused the successful authentication
      * @param response the response
      * @param authentication the Authentication object which was created during
      * the authentication process.
      */
     void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
             Authentication authentication) throws IOException, ServletException;
 }

       根据接口的描述信息也可以得知,登录成功后会自动回调这个方法,进一步查看它的默认实现,你会发现successForwardUrl、defaultSuccessUrl也是由它的子类实现的:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第1张图片

  • 在config包中自定义 AuthenticationSuccessHandler接口实现类MyAuthenticationSuccessHandler

 package com.study.config;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 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.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @ClassName MyAuthenticationSuccessHandler
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/7/23 20:30
  * @Version 1.0
  */
 public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
     @Override
     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         Map result = new HashMap();
         result.put("msg", "登录成功");//打印登录成功信息
         result.put("status", 200);//打印状态码
         result.put("authentication", authentication);//打印认证信息
         response.setContentType("application/json;charset=UTF-8");//设置响应类型
         String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
         response.getWriter().println(s);//打印json格式数据
     }
 }
  • 在WebSecurityConfigurer中配置MyAuthenticationSuccessHandler

 package com.study.config;
 ​
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 ​
 /**
  * @ClassName WebSecurityConfigurer
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/7/21 22:34
  * @Version 1.0
  */
 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         ......
                 //.successForwardUrl("/index") //认证成功后跳转路径 forward 跳转路径  始终在认证成功之后跳转到指定请求 地址栏不变
                 //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径 redirect 重定向  注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变
                 //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false
                 .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案
                 .and()
                 .csrf().disable();//此处先关闭CSRF跨站保护
     }
 }
 ​
  • 访问路径:http://localhost:8080/hello,输入用户名密码登录后,输出结果:

3.4 显示登录失败信息

配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

       为了能更直观地在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security 在登录失败之后会将异常信息存储到 requestsession作用域中key为 SPRING_SECURITY_LAST_EXCEPTION 命名属性中,源码可以参考 SimpleUrlAuthenticationFailureHandler :

 //
 // Source code recreated from a .class file by IntelliJ IDEA
 // (powered by FernFlower decompiler)
 //
 ​
 package org.springframework.security.web.authentication;
 ​
 import java.io.IOException;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.DefaultRedirectStrategy;
 import org.springframework.security.web.RedirectStrategy;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.util.Assert;
 ​
 public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
     protected final Log logger = LogFactory.getLog(this.getClass());
     private String defaultFailureUrl;
     private boolean forwardToDestination = false;
     private boolean allowSessionCreation = true;
     private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
 ​
     public SimpleUrlAuthenticationFailureHandler() {
     }
 ​
     public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
         this.setDefaultFailureUrl(defaultFailureUrl);
     }
 ​
     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
         if (this.defaultFailureUrl == null) {
             if (this.logger.isTraceEnabled()) {
                 this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
             } else {
                 this.logger.debug("Sending 401 Unauthorized error");
             }
 ​
             response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
         } else {
             this.saveException(request, exception);
             if (this.forwardToDestination) {
                 this.logger.debug("Forwarding to " + this.defaultFailureUrl);
                 request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
             } else {
                 this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
             }
 ​
         }
     }
 ​
     protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
         if (this.forwardToDestination) {
             request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
         } else {
             HttpSession session = request.getSession(false);
             if (session != null || this.allowSessionCreation) {
                 request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
             }
 ​
         }
     }
 ​
     public void setDefaultFailureUrl(String defaultFailureUrl) {
         Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), () -> {
             return "'" + defaultFailureUrl + "' is not a valid redirect URL";
         });
         this.defaultFailureUrl = defaultFailureUrl;
     }
 ​
     protected boolean isUseForward() {
         return this.forwardToDestination;
     }
 ​
     public void setUseForward(boolean forwardToDestination) {
         this.forwardToDestination = forwardToDestination;
     }
 ​
     public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
         this.redirectStrategy = redirectStrategy;
     }
 ​
     protected RedirectStrategy getRedirectStrategy() {
         return this.redirectStrategy;
     }
 ​
     protected boolean isAllowSessionCreation() {
         return this.allowSessionCreation;
     }
 ​
     public void setAllowSessionCreation(boolean allowSessionCreation) {
         this.allowSessionCreation = allowSessionCreation;
     }
 }
 ​

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第2张图片

  • login.html中添加显示异常信息代码

     
     
     
         
         登录页面
     
     
     

    用户登录

     ...  

               
     

       
  • WebSecurityConfigurer配置登录失败处理

     package com.study.config;
     ​
     import org.springframework.context.annotation.Configuration;
     import org.springframework.security.config.annotation.web.builders.HttpSecurity;
     import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
     ​
     /**
      * @ClassName WebSecurityConfigurer
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/7/21 22:34
      * @Version 1.0
      */
     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
                     .......
                     .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案
                     //.failureForwardUrl("/login.html")//认证失败之后,forward跳转
                     .failureUrl("/login.html") //默认认证失败之后,redirect跳转
                     .and()
                     .csrf().disable();//此处先关闭CSRF跨站保护
         }
     }
     ​

    注意:failureUrl、failureForwardUrl 关系类似于之前提到的 successForwardUrl 、defaultSuccessUrl 方法

    • failureUrl:失败以后的重定向跳转

    • failureForwardUrl:失败以后的 forward 跳转

      注意:因此获取 request 中异常信息,这里只能使用failureForwardUrl
  • 启动项目,测试路径:http://localhost:8080/hello,输入错误的用户名和密码时会输出如下错误信息,注意login.html与WebSecurityConfigurer对failureForwardUrl或failureUrl的配置一致:

(1)failureForwardUrl:forward跳转,request作用域

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第3张图片

(2)failureUrl:redirect跳转,session作用域

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第4张图片

3.5 自定义登录失败处理

配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

       和自定义登录成功处理一样,Spring Security 同样为前后端分离开发提供了登录失败的处理,这个类就是 AuthenticationFailureHandler,源码为:

 public interface AuthenticationFailureHandler {
 ​
     /**
      * Called when an authentication attempt fails.
      * @param request the request during which the authentication attempt occurred.
      * @param response the response.
      * @param exception the exception which was thrown to reject the authentication
      * request.
      */
     void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
             AuthenticationException exception) throws IOException, ServletException;
 ​
 }

       根据接口的描述信息,也可以得知登录失败会自动回调这个方法,进一步查看它的默认实现,你会发现failureUrl、failureForwardUrl也是由它的子类实现的。

  • config包中自定义 AuthenticationFailureHandler接口实现类MyAuthenticationFailureHandler

 package com.study.config;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 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.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @ClassName MyAuthenticationFailureHandler
  * @Description 自定义认证失败处理
  * @Author Jiangnan Cui
  * @Date 2022/7/24 15:54
  * @Version 1.0
  */
 public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
     @Override
     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
         Map result = new HashMap();
         result.put("msg", "登录失败:" + exception.getMessage());
         result.put("status", 500);
         response.setContentType("application/json;charset=UTF-8");
         String s = new ObjectMapper().writeValueAsString(result);
         response.getWriter().println(s);
     }
 }
 ​
  • WebSecurityConfigurer配置MyAuthenticationFailureHandler

 package com.study.config;
 ​
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 ​
 /**
  * @ClassName WebSecurityConfigurer
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/7/21 22:34
  * @Version 1.0
  */
 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .......
                 //.failureForwardUrl("/login.html")//认证失败之后,forward跳转
                 //.failureUrl("/login.html") //默认认证失败之后,redirect跳转
                 .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
                 .and()
                 .csrf().disable();//此处先关闭CSRF跨站保护
     }
 }
 ​
  • 测试路径:http://localhost:8080/hello,输入错误的用户名或密码后输出如下结果:

3.6 注销登录

配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

       Spring Security 中也提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。

  • 开启注销登录(默认开启)

     package com.study.config;
     ​
     import org.springframework.context.annotation.Configuration;
     import org.springframework.security.config.annotation.web.builders.HttpSecurity;
     import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
     import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
     import org.springframework.security.web.util.matcher.OrRequestMatcher;
     ​
     /**
      * @ClassName WebSecurityConfigurer
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/7/21 22:34
      * @Version 1.0
      */
     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
                     .......
                     .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
                     .and()
                     .logout()
                     .logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET
                     .invalidateHttpSession(true)//默认开启会话失效
                     .clearAuthentication(true)//默认清除认证标志
                     .logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面
                     .and()
                     .csrf().disable();//此处先关闭CSRF跨站保护
         }
     }
     ​

           测试注销路径:登录成功后,访问:http://localhost:8080/hello,通过访问:http://localhost:8080/logout,进行注销登录。

    注意:

    • 通过 logout() 方法开启注销配置

    • logoutUrl 指定退出登录请求地址,默认是 GET 请求,路径为 /logout

    • invalidateHttpSession 退出时是否使session 失效,默认值为 true

    • clearAuthentication 退出时是否清除认证信息,默认值为 true

    • logoutSuccessUrl 退出登录时跳转地址

  • 配置多个注销登录请求

    如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的方法:

     package com.study.config;
     ​
     import org.springframework.context.annotation.Configuration;
     import org.springframework.security.config.annotation.web.builders.HttpSecurity;
     import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
     import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
     import org.springframework.security.web.util.matcher.OrRequestMatcher;
     ​
     /**
      * @ClassName WebSecurityConfigurer
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/7/21 22:34
      * @Version 1.0
      */
     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
                     .......
                     .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
                     .and()
                     .logout()
                     //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET
                     .logoutRequestMatcher(new OrRequestMatcher(
                             new AntPathRequestMatcher("/aaa", "GET"),
                             new AntPathRequestMatcher("/bbb", "POST")
                     ))
                     .invalidateHttpSession(true)//默认开启会话失效
                     .clearAuthentication(true)//默认清除认证标志
                     .logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面
                     .and()
                     .csrf().disable();//此处先关闭CSRF跨站保护
         }
     }
     ​

    注意:

    • GET请求直接在地址栏访问,注销登录路径:http://localhost:8080/aaa

    • POST方式需要新建表单及对应的Controller,注销登录路径:http://localhost:8080/logout.html

      • logout.html

         
         
         
             
             注销页面
         
         
         

        用户注销

         
               
           
      • LogoutController

         package com.study.controller;
         ​
         import org.springframework.stereotype.Controller;
         import org.springframework.web.bind.annotation.RequestMapping;
         ​
         /**
          * @ClassName LogoutController
          * @Description TODO
          * @Author Jiangnan Cui
          * @Date 2022/7/24 18:15
          * @Version 1.0
          */
         @Controller
         public class LogoutController {
           @RequestMapping("logout.html")
             public String logout() {
                 return "logout";
             }
         }
  • 前后端分离注销登录配置

    配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili

           如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义 LogoutSuccessHandler 实现来返回注销之后信息:

    • 编写LogoutSuccessHandler接口实现类MyLogoutSuccessHandler

     package com.study.config;
     ​
     import com.fasterxml.jackson.databind.ObjectMapper;
     import org.springframework.security.core.Authentication;
     import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
     ​
     import javax.servlet.ServletException;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
     import java.io.IOException;
     import java.util.HashMap;
     import java.util.Map;
     ​
     /**
      * @ClassName MyLogoutSuccessHandler
      * @Description 自定义认证注销处理
      * @Author Jiangnan Cui
      * @Date 2022/7/23 20:30
      * @Version 1.0
      */
     public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
         @Override
         public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
             Map result = new HashMap();
             result.put("msg", "注销成功,当前认证对象为:" + authentication);//打印认证信息
             result.put("status", 200);//打印状态码
             response.setContentType("application/json;charset=UTF-8");//设置响应类型
             String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
             response.getWriter().println(s);//打印json格式数据
         }
     }
     ​
    • WebSecurityConfigurer配置MyLogoutSuccessHandler

     package com.study.config;
     ​
     import org.springframework.context.annotation.Configuration;
     import org.springframework.security.config.annotation.web.builders.HttpSecurity;
     import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
     import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
     import org.springframework.security.web.util.matcher.OrRequestMatcher;
     ​
     /**
      * @ClassName WebSecurityConfigurer
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/7/21 22:34
      * @Version 1.0
      */
     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.authorizeRequests()
                     .......
                     .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
                     .and()
                     .logout()
                     //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET
                     .logoutRequestMatcher(new OrRequestMatcher(
                             new AntPathRequestMatcher("/aaa", "GET"),
                             new AntPathRequestMatcher("/bbb", "POST")
                     ))
                     .invalidateHttpSession(true)//默认开启会话失效
                     .clearAuthentication(true)//默认清除认证标志
                     //.logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面
                     .logoutSuccessHandler(new MyLogoutSuccessHandler())
                     .and()
                     .csrf().disable();//此处先关闭CSRF跨站保护
         }
     }
    

测试路径:http://localhost:8080/aaa

测试路径:http://localhost:8080/logout.html

3.7 登录用户数据获取

配套视频:18.获取用户认证信息_哔哩哔哩_bilibili

3.7.1 SecurityContextHolder介绍

       Spring Security 会将登录用户数据保存在 Session 中,但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定:当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。

       SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。

       实际上 SecurityContextHolder 中存储是 SecurityContext,在 SecurityContext 中存储是 Authentication。

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第5张图片

这种设计是典型的策略设计模式:

 public class SecurityContextHolder {
     public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
     public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
     public static final String MODE_GLOBAL = "MODE_GLOBAL";
     private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
     private static SecurityContextHolderStrategy strategy;
   //....
     private static void initializeStrategy() {
         if (MODE_PRE_INITIALIZED.equals(strategyName)) {
             Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
                     + ", setContextHolderStrategy must be called with the fully constructed strategy");
             return;
         }
         if (!StringUtils.hasText(strategyName)) {
             // Set default
             strategyName = MODE_THREADLOCAL;
         }
         if (strategyName.equals(MODE_THREADLOCAL)) {
             strategy = new ThreadLocalSecurityContextHolderStrategy();
             return;
         }
         if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
             strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
             return;
         }
         if (strategyName.equals(MODE_GLOBAL)) {
             strategy = new GlobalSecurityContextHolderStrategy();
             return;
         }
     //.....
   }
 }

三种策略详细解释:

  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的,这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。

  2. MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么就可以使用这种存储模式,使用时需要在VM Options单独进行配置(实际上是将父线程的用户数据复制一份到子线程)。

  3. MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到(了解即可)。

3.7.2 SecurityContextHolderStrategy介绍

       通过 SecurityContextHolder 可以得知,SecurityContextHolderStrategy 接口用来定义存储策略方法:

 public interface SecurityContextHolderStrategy {
     void clearContext();
     SecurityContext getContext();
     void setContext(SecurityContext context);
     SecurityContext createEmptyContext();
 }

接口中一共定义了四个方法:

  • clearContext:该方法用来清除存储的 SecurityContext对象。

  • getContext:该方法用来获取存储的 SecurityContext 对象。

  • setContext:该方法用来设置存储的 SecurityContext 对象。

  • createEmptyContext:该方法则用来创建一个空的 SecurityContext 对象。

从上面可以看出每一个实现类对应一种策略的实现。

3.7.3 代码中获取认证之后的用户数据

 @RestController
 public class HelloController {
     @RequestMapping("/hello")
     public String hello() {
         System.out.println("Hello Spring Security!");
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//获得认证信息
         System.out.println("authentication.getName() = " + authentication.getName());
         System.out.println("authentication.getAuthorities() = " + authentication.getAuthorities());
         System.out.println("authentication.getCredentials() = " + authentication.getCredentials());
         System.out.println("authentication.getDetails() = " + authentication.getDetails());
         System.out.println("authentication.getPrincipal() = " + authentication.getPrincipal());
 ​
         User principal = (User) authentication.getPrincipal();//返回值类型为User时需要进行强制转换 org.springframework.security.core.userdetails.User
         System.out.println("principal.getUsername() = " + principal.getUsername());
         System.out.println("principal.getPassword() = " + principal.getPassword());
         System.out.println("principal.getAuthorities() = " + principal.getAuthorities());
         return "hello spring security!";
     }
 }
 ​

启动服务,登录成功后访问:http://localhost:8080/hello,获得认证之后的用户数据。

IDEA控制台输出结果:

 Hello Spring Security!
 authentication.getName() = root
 authentication.getAuthorities() = [ROLE_admin, ROLE_super]
 authentication.getCredentials() = null
 authentication.getDetails() = WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=15FEA49E4810C435286FD8B6D188C87F]
 authentication.getPrincipal() = org.springframework.security.core.userdetails.User [Username=root, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_admin, ROLE_super]]
 principal.getUsername() = root
 principal.getPassword() = null
 principal.getAuthorities() = [ROLE_admin, ROLE_super]

3.7.4 多线程情况下获取用户数据

 package com.study.controller;
 ​
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 /**
  * @ClassName HelloController
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/7/6 12:12
  * @Version 1.0
  */
 @RestController
 public class HelloController {
     @RequestMapping("/hello")
     public String hello() {
         System.out.println("Hello Spring Security!");
         new Thread(() -> {
             Authentication Authentication = SecurityContextHolder.getContext().getAuthentication();
             User childPrincipal = (User) Authentication.getPrincipal();
             System.out.println("Principal.getUsername() = " + Principal.getUsername());
             System.out.println("Principal.getPassword() = " + Principal.getPassword());
             System.out.println("Principal.getAuthorities() = " + Principal.getAuthorities());
         }).start();
         return "hello spring security!";
     }
 }
 ​

启动服务,访问:http://localhost:8080/hello,获得认证之后的用户数据。

视频中报错:

实际测试报错:

 Exception in thread "Thread-2" java.lang.NullPointerException
     at com.study.controller.HelloController.lambda$hello$0(HelloController.java:35)
     at java.lang.Thread.run(Thread.java:750)

       从上面可以看到默认策略,是无法在子线程中获取用户信息,如果需要在子线程中获取必须使用第二种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进行修改。

 -Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第6张图片启动服务,访问:http://localhost:8080/hello,获得认证之后的用户数据。

IDEA控制台输出结果:

 Hello Spring Security!
 Principal.getUsername() = root
 Principal.getPassword() = null
 Principal.getAuthorities() = [ROLE_admin, ROLE_super]

3.7.5 页面上获取用户信息

配套视频:19.页面中获取用户认证信息_哔哩哔哩_bilibili

  • pom.xml引入依赖

     
       org.thymeleaf.extras
       thymeleaf-extras-springsecurity5
       3.0.4.RELEASE
     
  • logout.html页面加入命名空间

     
  • logout.html页面中获取用户信息

     
     
         
    •    
    •    
    •    
    •    
    •  

启动服务,登录成功后访问logout.html:http://localhost:8080/logout.html

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第7张图片

3.8 自定义认证数据源

配套视频:20.自定义数据源之认证流程分析_哔哩哔哩_bilibili

3.8.1 认证流程分析

Servlet Authentication Architecture :: Spring Security

  • 发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter 拦截

  • UsernamePasswordAuthenticationFilterattemptAuthentication方法中将请求中用户名和密码,封装为Authentication对象,并交给AuthenticationManager 进行认证

  • 认证成功,将认证信息存储到 SecurityContextHodler 以及调用记住我等,并回调 AuthenticationSuccessHandler 处理

  • 认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler 处理

3.8.2 AuthenticationManager、ProviderManager 、AuthenticationProvider三者关系

       从上面分析中得知,AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?

  • AuthenticationManager:是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。

  • ProviderManager:AuthenticationManager接口的实现类,Spring Security 认证时默认使用就是的 ProviderManager。

  • AuthenticationProvider:就是针对不同的身份类型执行的具体的身份认证。

(1)AuthenticationManager 与 ProviderManager:父子关系

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第8张图片

        ProviderManager 是 AuthenticationManager 的唯一实现(其它实现要么是私有的,要么是静态的,不能被使用),也是 Spring Security 默认使用的实现。从这里不难看出,默认情况下AuthenticationManager 就是一个ProviderManager。

(2)ProviderManager 与 AuthenticationProvider

摘自官方: Servlet Authentication Architecture :: Spring Security

       在 Spring Security 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个 AuthenticationProvider 来提供。

       多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。换句话说,在ProviderManager 中存在一个 AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每一个 AuthenticationProvider 去执行身份认证,最终得到认证结果。

       ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,ProviderManager 的 parent 可以是任意类型的 AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent。

       ProviderManager 本身也可以有多个,多个ProviderManager 共用同一个 parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用 AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级。然后,父级是一种 全局资源,作为所有提供者的后备资源。

       根据上面的介绍,我们绘出新的 AuthenticationManager、ProvideManager 和 AuthentictionProvider 关系:

摘自官网: Topical Guide | Spring Security Architecture

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第9张图片

配套视频:21.自定义数据源之原理分析 (一)_哔哩哔哩_bilibili

       弄清楚认证原理之后我们来看下具体认证时数据源的获取:默认情况下,AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据源的校验。他们之间调用关系如下:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第10张图片       总结: AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式,认证时底层数据源需要调用 UserDetailService 来实现。

3.8.3 配置全局 AuthenticationManager

配套视频:22.自定义数据源之原理分析(二)_哔哩哔哩_bilibili

23.自定义数据源之全局配置AuthenticationManager方式_哔哩哔哩_bilibili

官方网站:Topical Guide | Spring Security Architecture

  • 默认的全局 AuthenticationManager:springboot 对 security 进行自动配置时,自动在工厂中创建一个全局AuthenticationManager

     //springboot对security默认配置中,在工厂中默认创建AuthenticationManager
     @Autowired //此段代码是从官网复制来的
     public void initialize(AuthenticationManagerBuilder builder) throws Exception {
         System.out.println("springboot默认配置: " + builder);
     }
     /**
      * 输出结果:
      *    springboot默认配置: org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@60297f36
      */

    总结:默认自动配置创建的全局AuthenticationManager

    • 默认查找当前项目中是否存在自定义的UserDetailService 实例,自动将当前项目UserDetailService 实例设置为数据源;

    • 在工厂中使用时直接在代码中注入即可。

  • 自定义全局 AuthenticationManager

     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
       @Override
       public void configure(AuthenticationManagerBuilder builder) {
         //builder ....
       }
     }

    总结:一旦通过 configure 方法自定义AuthenticationManager的实现

    1. 就会将工厂中自动配置的AuthenticationManager进行覆盖;

    2. 需要在实现中指定认证数据源对象UserDetaiService实例;

    3. 这种方式创建的AuthenticationManager对象是工厂内部本地的一个 AuthenticationManager对象,不允许在其他自定义组件中进行注入。

  • 用来在工厂中暴露自定义AuthenticationManager 实例

     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
       
         //1.自定义AuthenticationManager  推荐使用这种方式,而不是直接使用默认自动配置的,但这种方式并没有在工厂中暴露出来
         @Override
         public void configure(AuthenticationManagerBuilder builder) throws Exception {
             System.out.println("自定义AuthenticationManager: " + builder);
             builder.userDetailsService(userDetailsService());
         }
     ​
         //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
         @Override
         @Bean //记得加上此注解,将对象注入工厂
         public AuthenticationManager authenticationManagerBean() throws Exception {
             return super.authenticationManagerBean();
         }
     }
     ​

测试代码总结:

 package com.study.config;
 ​
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.authentication.AuthenticationManager;
 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.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 ​
 /**
  * @ClassName WebSecurityConfigurer
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/7/21 22:34
  * @Version 1.0
  */
 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
     //1.默认AuthenticationManager:springboot对security默认配置中,在工厂中默认创建AuthenticationManager
 //    @Autowired //代码从官网复制而来
 //    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
 //        System.out.println("springboot默认配置:" + builder);
 //        builder.userDetailsService(userDetailsService());
 //    }
 ​
     /**
      * 未定义UserDetailsService前:启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功
      * 输出结果:
      *    springboot默认配置:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@68ace111
      */
 ​
     //在工厂中自定义userDetailsService
     @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
         userDetailsService.createUser(User.withUsername("master").password("{noop}123456").roles("admin").build());//noop表示明文
         return userDetailsService;
     }
 ​
     /**
      * 定义UserDetailsService后启动服务测试结果:
      *    上面这段(1.默认AuthenticationManager:)默认可以不写,写的时候会报错:
      *    org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'webSecurityConfigurer':
      *    Requested bean is currently in creation: Is there an unresolvable circular reference?
      * 这段代码显得多余了
      */
 ​
     //2.自定义AuthenticationManager 推荐使用这种自定义方式,但并没有在工厂中进行暴露
     @Override
     public void configure(AuthenticationManagerBuilder builder) throws Exception {
         System.out.println("自定义AuthenticationManager:" + builder);
         builder.userDetailsService(userDetailsService());
     }
     /**
      * 启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功
      * 输出结果:
      *    自定义AuthenticationManager:org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$2@24fabd0f
      */
 ​
     //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
     @Override
     @Bean
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
     
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .mvcMatchers("/index").permitAll() //放行/index请求
                 .mvcMatchers("/login.html").permitAll() //放行/login.html请求
                 .anyRequest().authenticated() //其它请求需要登录认证后才能访问
                 .and()
                 .formLogin() //默认form表单页面登录
                 .loginPage("/login.html") //使用自定义登录页面登录页面登录
                 .loginProcessingUrl("/doLogin") //使用自定义登录页面时需要重新指定url,对应login.html中的action路径
                 .usernameParameter("uname") //重新指定用户名名称
                 .passwordParameter("pwd") //重新指定密码名称
                 //.successForwardUrl("/index") //认证成功后跳转路径
                 //forward 跳转路径  始终在认证成功之后跳转到指定请求 地址栏不变
                 //.defaultSuccessUrl("/hello") //默认认证成功后跳转路径
                 //.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false
                 //redirect 重定向  注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变
                 .successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案
                 //.failureForwardUrl("/login.html")//认证失败之后,forward跳转
                 //.failureUrl("/login.html") //默认认证失败之后,redirect跳转
                 .failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
                 .and()
                 .logout()
                 //.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET
                 .logoutRequestMatcher(new OrRequestMatcher(
                         new AntPathRequestMatcher("/aaa", "GET"),
                         new AntPathRequestMatcher("/bbb", "POST")
                 ))
                 .invalidateHttpSession(true)//默认开启会话失效
                 .clearAuthentication(true)//默认清除认证标志
                 //.logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面
                 .logoutSuccessHandler(new MyLogoutSuccessHandler())
                 .and()
                 .csrf().disable();//此处先关闭CSRF跨站保护
     }
 }
 ​

3.8.4 自定义内存数据源

配套视频:24.自定义数据源之全局配置AuthenticationManager总结_哔哩哔哩_bilibili

通过上面分析可以重点在于UserDetailsService,查看UserDetailsService接口如下:

 public interface UserDetailsService {
     UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
 }

里面只有一个要实现的方法loadUserByUsername,返回类型为UserDetails,查看UserDetails定义如下:

 public interface UserDetails extends Serializable {
     Collection getAuthorities();
     String getPassword();
     String getUsername();
     boolean isAccountNonExpired();
     boolean isAccountNonLocked();
     boolean isCredentialsNonExpired();
     boolean isEnabled();
 }

通过在UserDetails上Ctrl+H可以在右侧查看其实现类User:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第11张图片User定义如下:

 //
 // Source code recreated from a .class file by IntelliJ IDEA
 // (powered by FernFlower decompiler)
 //
 ​
 package org.springframework.security.core.userdetails;
 ​
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.function.Function;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.springframework.security.core.CredentialsContainer;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.crypto.factory.PasswordEncoderFactories;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.util.Assert;
 ​
 public class User implements UserDetails, CredentialsContainer {
     private static final long serialVersionUID = 560L;
     private static final Log logger = LogFactory.getLog(User.class);
     private String password;
     private final String username;
     private final Set authorities;
     private final boolean accountNonExpired;
     private final boolean accountNonLocked;
     private final boolean credentialsNonExpired;
     private final boolean enabled;
 ​
     public User(String username, String password, Collection authorities) {
         this(username, password, true, true, true, true, authorities);
     }
 ​
     public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) {
         Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
         this.username = username;
         this.password = password;
         this.enabled = enabled;
         this.accountNonExpired = accountNonExpired;
         this.credentialsNonExpired = credentialsNonExpired;
         this.accountNonLocked = accountNonLocked;
         this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
     }
 ​
     public Collection getAuthorities() {
         return this.authorities;
     }
 ​
     public String getPassword() {
         return this.password;
     }
 ​
     public String getUsername() {
         return this.username;
     }
 ​
     public boolean isEnabled() {
         return this.enabled;
     }
 ​
     public boolean isAccountNonExpired() {
         return this.accountNonExpired;
     }
 ​
     public boolean isAccountNonLocked() {
         return this.accountNonLocked;
     }
 ​
     public boolean isCredentialsNonExpired() {
         return this.credentialsNonExpired;
     }
 ​
     public void eraseCredentials() {
         this.password = null;
     }
 ​
     private static SortedSet sortAuthorities(Collection authorities) {
         Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
         SortedSet sortedAuthorities = new TreeSet(new AuthorityComparator());
         Iterator var2 = authorities.iterator();
 ​
         while(var2.hasNext()) {
             GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next();
             Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
             sortedAuthorities.add(grantedAuthority);
         }
 ​
         return sortedAuthorities;
     }
 ​
     public boolean equals(Object obj) {
         return obj instanceof User ? this.username.equals(((User)obj).username) : false;
     }
 ​
     public int hashCode() {
         return this.username.hashCode();
     }
 ​
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append(this.getClass().getName()).append(" [");
         sb.append("Username=").append(this.username).append(", ");
         sb.append("Password=[PROTECTED], ");
         sb.append("Enabled=").append(this.enabled).append(", ");
         sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", ");
         sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", ");
         sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", ");
         sb.append("Granted Authorities=").append(this.authorities).append("]");
         return sb.toString();
     }
 ​
     public static UserBuilder withUsername(String username) {
         return builder().username(username);
     }
 ​
     public static UserBuilder builder() {
         return new UserBuilder();
     }
 ​
     /** @deprecated */
     @Deprecated
     public static UserBuilder withDefaultPasswordEncoder() {
         logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.");
         PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
         UserBuilder var10000 = builder();
         Objects.requireNonNull(encoder);
         return var10000.passwordEncoder(encoder::encode);
     }
 ​
     public static UserBuilder withUserDetails(UserDetails userDetails) {
         return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled());
     }
 ​
     public static final class UserBuilder {
         private String username;
         private String password;
         private List authorities;
         private boolean accountExpired;
         private boolean accountLocked;
         private boolean credentialsExpired;
         private boolean disabled;
         private Function passwordEncoder;
 ​
         private UserBuilder() {
             this.passwordEncoder = (password) -> {
                 return password;
             };
         }
 ​
         public UserBuilder username(String username) {
             Assert.notNull(username, "username cannot be null");
             this.username = username;
             return this;
         }
 ​
         public UserBuilder password(String password) {
             Assert.notNull(password, "password cannot be null");
             this.password = password;
             return this;
         }
 ​
         public UserBuilder passwordEncoder(Function encoder) {
             Assert.notNull(encoder, "encoder cannot be null");
             this.passwordEncoder = encoder;
             return this;
         }
 ​
         public UserBuilder roles(String... roles) {
             List authorities = new ArrayList(roles.length);
             String[] var3 = roles;
             int var4 = roles.length;
 ​
             for(int var5 = 0; var5 < var4; ++var5) {
                 String role = var3[var5];
                 Assert.isTrue(!role.startsWith("ROLE_"), () -> {
                     return role + " cannot start with ROLE_ (it is automatically added)";
                 });
                 authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
             }
 ​
             return this.authorities((Collection)authorities);
         }
 ​
         public UserBuilder authorities(GrantedAuthority... authorities) {
             return this.authorities((Collection)Arrays.asList(authorities));
         }
 ​
         public UserBuilder authorities(Collection authorities) {
             this.authorities = new ArrayList(authorities);
             return this;
         }
 ​
         public UserBuilder authorities(String... authorities) {
             return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities));
         }
 ​
         public UserBuilder accountExpired(boolean accountExpired) {
             this.accountExpired = accountExpired;
             return this;
         }
 ​
         public UserBuilder accountLocked(boolean accountLocked) {
             this.accountLocked = accountLocked;
             return this;
         }
 ​
         public UserBuilder credentialsExpired(boolean credentialsExpired) {
             this.credentialsExpired = credentialsExpired;
             return this;
         }
 ​
         public UserBuilder disabled(boolean disabled) {
             this.disabled = disabled;
             return this;
         }
 ​
         public UserDetails build() {
             String encodedPassword = (String)this.passwordEncoder.apply(this.password);
             return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities);
         }
     }
 ​
     private static class AuthorityComparator implements Comparator, Serializable {
         private static final long serialVersionUID = 560L;
 ​
         private AuthorityComparator() {
         }
 ​
         public int compare(GrantedAuthority g1, GrantedAuthority g2) {
             if (g2.getAuthority() == null) {
                 return -1;
             } else {
                 return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());
             }
         }
     }
 }
 ​

主要关注User的几个成员变量:

 private String password;
 private final String username;
 private final Set authorities;
 private final boolean accountNonExpired;
 private final boolean accountNonLocked;
 private final boolean credentialsNonExpired;
 private final boolean enabled;

举例:

 @Configuration
 public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
 ​
     @Bean
     public UserDetailsService userDetailsService(){
         InMemoryUserDetailsManager inMemoryUserDetailsManager
                 = new InMemoryUserDetailsManager();
         UserDetails u1 = User.withUsername("zhangs")
                 .password("{noop}111").roles("USER").build();
         inMemoryUserDetailsManager.createUser(u1);
         return inMemoryUserDetailsManager;
     }
 ​
     @Override
     protected void configure(AuthenticationManagerBuilder auth) 
       throws Exception {
         auth.userDetailsService(userDetailsService());
     }   
 }

3.8.5 自定义数据库数据源

配套视频:25.自定义数据源之库表设计(一)_哔哩哔哩_bilibili

26.自定义数据源之实现(二)_哔哩哔哩_bilibili

  • 设计表结构

     -- 用户表 共有多个用户
     CREATE TABLE `user`
     (
         `id`                    int(11) NOT NULL AUTO_INCREMENT,
         `username`              varchar(32)  DEFAULT NULL,
         `password`              varchar(255) DEFAULT NULL,
         `enabled`               tinyint(1) DEFAULT NULL,
         `accountNonExpired`     tinyint(1) DEFAULT NULL,
         `accountNonLocked`      tinyint(1) DEFAULT NULL,
         `credentialsNonExpired` tinyint(1) DEFAULT NULL,
         PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
     ​
     -- 角色表 共有多个角色,其中,_zh表示中文
     CREATE TABLE `role`
     (
         `id`      int(11) NOT NULL AUTO_INCREMENT,
         `name`    varchar(32) DEFAULT NULL,
         `name_zh` varchar(32) DEFAULT NULL,
         PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
      
     -- 用户角色关系表:用户与角色为多对多关系,需要建立中间表
     CREATE TABLE `user_role`
     (
         `id`  int(11) NOT NULL AUTO_INCREMENT,
         `uid` int(11) DEFAULT NULL,
         `rid` int(11) DEFAULT NULL,
         PRIMARY KEY (`id`),
         KEY   `uid` (`uid`),
         KEY   `rid` (`rid`)
     ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
  • Navicat新建MySQL数据库security,字符集utf8mb4:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第12张图片

  • security数据库上,右键新建查询: 【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第13张图片

执行SQL语句:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第14张图片

  • 插入测试数据

     -- 插入用户数据
     BEGIN;
       INSERT INTO `user`
       VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
       INSERT INTO `user`
       VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
       INSERT INTO `user`
       VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
     COMMIT;
     ​
     -- 插入角色数据 授权时ROLE_进行处理,RBAC
     BEGIN;
       INSERT INTO `role`
       VALUES (1, 'ROLE_product', '商品管理员');
       INSERT INTO `role`
       VALUES (2, 'ROLE_admin', '系统管理员');
       INSERT INTO `role`
       VALUES (3, 'ROLE_user', '用户管理员');
     COMMIT;
     ​
     -- 插入用户角色数据
     BEGIN;
       INSERT INTO `user_role`
       VALUES (1, 1, 1);
       INSERT INTO `user_role`
       VALUES (2, 1, 2);
       INSERT INTO `user_role`
       VALUES (3, 2, 2);
       INSERT INTO `user_role`
       VALUES (4, 3, 3);
     COMMIT;
  • security数据库上,右键新建查询,插入数据:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第15张图片

插入后查看表中是否包含数据:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第16张图片

接下来进行SpringBoot整合mybatis

  • 项目的pom.xml中引入依赖

     
     
         com.alibaba
         druid
         1.2.7
     
     ​
     
     
         mysql
         mysql-connector-java
         5.1.38
     
     ​
     
     
         org.mybatis.spring.boot
         mybatis-spring-boot-starter
         2.2.0
     
  • application.properties进行数据源、mybatis、日志配置

     # datasource:类型、驱动名、用户名、密码
     spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
     spring.datasource.driver-class-name=com.mysql.jdbc.Driver
     spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
     spring.datasource.username=root
     spring.datasource.password=root
     ​
     # mybatis配置mapper文件的位置和别名设置
     # 注意mapper目录(包)新建时必须使用"/",而不是.
     mybatis.mapper-locations=classpath:com/study/mapper/*.xml
     mybatis.type-aliases-package=com.study.entity
     ​
     # log:为了显示mybatis运行SQL语句
     logging.level.com.study=debug
  • 创建 entity实体类包

    • 创建User 对象

       package com.study.entity;
       ​
       import org.springframework.security.core.GrantedAuthority;
       import org.springframework.security.core.authority.SimpleGrantedAuthority;
       import org.springframework.security.core.userdetails.UserDetails;
       ​
       import java.util.*;
       ​
       /**
        * @ClassName User
        * @Description 用户类,对应表user,为了与UserDetails中的User对应,此处进行实现
        * @Author Jiangnan Cui
        * @Date 2022/8/7 11:43
        * @Version 1.0
        */
       public class User implements UserDetails {
           private Integer id;
           private String username;
           private String password;
           private Boolean enabled;
           private Boolean accountNonExpired;
           private Boolean accountNonLocked;
           private Boolean credentialsNonExpired;
           private List roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息
       ​
           //返回权限信息
           @Override
           public Collection getAuthorities() {
               Set authorities = new HashSet<>();
               roles.forEach(role -> {
                   SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
                   authorities.add(simpleGrantedAuthority);
               });
               return authorities;
           }
       ​
           @Override
           public String getPassword() {
               return password;
           }
       ​
           public void setPassword(String password) {
               this.password = password;
           }
       ​
           @Override
           public String getUsername() {
               return username;
           }
       ​
           public void setUsername(String username) {
               this.username = username;
           }
       ​
           @Override
           public boolean isAccountNonExpired() {
               return accountNonLocked;
           }
       ​
           @Override
           public boolean isAccountNonLocked() {
               return accountNonLocked;
           }
       ​
           @Override
           public boolean isCredentialsNonExpired() {
               return credentialsNonExpired;
           }
       ​
           @Override
           public boolean isEnabled() {
               return enabled;
           }
       ​
           public Integer getId() {
               return id;
           }
       ​
           public void setId(Integer id) {
               this.id = id;
           }
       ​
           public Boolean getEnabled() {
               return enabled;
           }
       ​
           public void setEnabled(Boolean enabled) {
               this.enabled = enabled;
           }
       ​
           public Boolean getAccountNonExpired() {
               return accountNonExpired;
           }
       ​
           public void setAccountNonExpired(Boolean accountNonExpired) {
               this.accountNonExpired = accountNonExpired;
           }
       ​
           public Boolean getAccountNonLocked() {
               return accountNonLocked;
           }
       ​
           public void setAccountNonLocked(Boolean accountNonLocked) {
               this.accountNonLocked = accountNonLocked;
           }
       ​
           public Boolean getCredentialsNonExpired() {
               return credentialsNonExpired;
           }
       ​
           public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
               this.credentialsNonExpired = credentialsNonExpired;
           }
       ​
           public List getRoles() {
               return roles;
           }
       ​
           public void setRoles(List roles) {
               this.roles = roles;
           }
       }
    • 创建 Role 对象

       package com.study.entity;
       ​
       /**
        * @ClassName Role
        * @Description 角色类,对应表role
        * @Author Jiangnan Cui
        * @Date 2022/8/7 11:44
        * @Version 1.0
        */
       public class Role {
           private Integer id;
           private String name;
           private String nameZh;
       ​
           public Integer getId() {
               return id;
           }
       ​
           public void setId(Integer id) {
               this.id = id;
           }
       ​
           public String getName() {
               return name;
           }
       ​
           public void setName(String name) {
               this.name = name;
           }
       ​
           public String getNameZh() {
               return nameZh;
           }
       ​
           public void setNameZh(String nameZh) {
               this.nameZh = nameZh;
           }
       }
       ​
  • 创建 UserDao 接口

     package com.study.dao;
     ​
     import com.study.entity.Role;
     import com.study.entity.User;
     import org.apache.ibatis.annotations.Mapper;
     ​
     import java.util.List;
     ​
     /**
      * @ClassName UserDao
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/8/7 12:09
      * @Version 1.0
      */
     @Mapper
     public interface UserDao {
         /**
          * 根据用户名查找用户
          * @param username
          * @return
          */
         User loadUserByUsername(String username);
     ​
         /**
          * 根据用户id查询一个角色,注意一个用户可能不止一种角色
          * @param uid
          * @return
          */
         List getRolesByUid(Integer uid);
     }
  • 创建 UserMapper 实现

     
     
     ​
     
         
         
     ​
         
         
     
  • 创建 UserDetailService 实例

     package com.study.service;
     ​
     import com.study.dao.UserDao;
     import com.study.entity.Role;
     import com.study.entity.User;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.security.core.userdetails.UserDetails;
     import org.springframework.security.core.userdetails.UserDetailsService;
     import org.springframework.security.core.userdetails.UsernameNotFoundException;
     import org.springframework.stereotype.Service;
     import org.springframework.util.ObjectUtils;
     ​
     import java.util.List;
     ​
     /**
      * @ClassName MyUserDetailsService
      * @Description TODO
      * @Author Jiangnan Cui
      * @Date 2022/8/7 11:26
      * @Version 1.0
      */
     @Service
     public class MyUserDetailsService implements UserDetailsService {
         private UserDao userDao;
     ​
         @Autowired
         public MyUserDetailsService(UserDao userDao) {
             this.userDao = userDao;
         }
     ​
         @Override
         public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
             //1.查询用户
             User user = userDao.loadUserByUsername(username);
             if (ObjectUtils.isEmpty(user))
                 throw new RuntimeException("用户不存在");
             //2.查询权限信息
             List roles = userDao.getRolesByUid(user.getId());
             user.setRoles(roles);
             return user;
         }
     }
  • 配置 authenticationManager 使用自定义UserDetailService

     @Configuration
     public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
       
         private final UserDetailsService userDetailsService;
     ​
         @Autowired
         public WebSecurityConfigurer(UserDetailsService userDetailsService) {
             this.userDetailsService = userDetailsService;
         }
     ​
         @Override
         protected void configure(AuthenticationManagerBuilder builder) throws Exception {
             builder.userDetailsService(userDetailsService);
         }
       
         
         @Override
         protected void configure(HttpSecurity http) throws Exception {
           //web security..
         }
     }
  • 启动测试:访问http://localhost:8080/login.html,登录数据库中的用户可正常访问,显示登录成功,用户不存在时抛出异常!

3.9 传统web开发认证总结案例

配套视频:27.传统web开发认证总结案例_哔哩哔哩_bilibili

3.9.1 新建项目

新建module:spring-security-03web,引入Spring Web。

3.9.2 pom.xml引入依赖

 
 
     org.springframework.boot
     spring-boot-starter-thymeleaf
 
 ​
 
 
     org.springframework.boot
     spring-boot-starter-security
 
 ​
 
 
     org.thymeleaf.extras
     thymeleaf-extras-springsecurity5
     3.0.4.RELEASE
 
 ​
 
 
     com.alibaba
     druid
     1.2.7
 
 ​
 
 
     mysql
     mysql-connector-java
     5.1.38
 
 ​
 
 
     org.mybatis.spring.boot
     mybatis-spring-boot-starter
     2.2.0
 

3.9.3 application.properties添加配置

 # 配置Thymeleaf
 spring.thymeleaf.cache=false
 spring.thymeleaf.prefix=classpath:/templates/
 spring.thymeleaf.suffix=.html
 spring.thymeleaf.encoding=UTF-8
 spring.thymeleaf.mode=HTML
 # datasource:类型、驱动名、用户名、密码
 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
 spring.datasource.username=root
 spring.datasource.password=root
 # mybatis配置mapper文件的位置和别名设置
 # 注意mapper目录(包)新建时必须使用"/",而不是.
 mybatis.mapper-locations=classpath:com/study/mapper/*.xml
 mybatis.type-aliases-package=com.study.entity
 # log:为了显示mybatis运行SQL语句
 logging.level.com.study=debug

3.9.4 创建实体类entity

  • User

 package com.study.entity;
 ​
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 ​
 import java.util.*;
 ​
 /**
  * @ClassName User
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/7 15:16
  * @Version 1.0
  */
 public class User implements UserDetails {
     private Integer id;
     private String username;
     private String password;
     private Boolean enabled;
     private Boolean accountNonExpired;
     private Boolean accountNonLocked;
     private Boolean credentialsNonExpired;
     private List roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息
 ​
     //返回权限信息
     @Override
     public Collection getAuthorities() {
         Set authorities = new HashSet<>();
         roles.forEach(role -> {
             SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
             authorities.add(simpleGrantedAuthority);
         });
         return authorities;
     }
 ​
     @Override
     public String getPassword() {
         return password;
     }
 ​
     public void setPassword(String password) {
         this.password = password;
     }
 ​
     @Override
     public String getUsername() {
         return username;
     }
 ​
     public void setUsername(String username) {
         this.username = username;
     }
 ​
     @Override
     public boolean isAccountNonExpired() {
         return accountNonLocked;
     }
 ​
     @Override
     public boolean isAccountNonLocked() {
         return accountNonLocked;
     }
 ​
     @Override
     public boolean isCredentialsNonExpired() {
         return credentialsNonExpired;
     }
 ​
     @Override
     public boolean isEnabled() {
         return enabled;
     }
 ​
     public Integer getId() {
         return id;
     }
 ​
     public void setId(Integer id) {
         this.id = id;
     }
 ​
     public Boolean getEnabled() {
         return enabled;
     }
 ​
     public void setEnabled(Boolean enabled) {
         this.enabled = enabled;
     }
 ​
     public Boolean getAccountNonExpired() {
         return accountNonExpired;
     }
 ​
     public void setAccountNonExpired(Boolean accountNonExpired) {
         this.accountNonExpired = accountNonExpired;
     }
 ​
     public Boolean getAccountNonLocked() {
         return accountNonLocked;
     }
 ​
     public void setAccountNonLocked(Boolean accountNonLocked) {
         this.accountNonLocked = accountNonLocked;
     }
 ​
     public Boolean getCredentialsNonExpired() {
         return credentialsNonExpired;
     }
 ​
     public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
         this.credentialsNonExpired = credentialsNonExpired;
     }
 ​
     public List getRoles() {
         return roles;
     }
 ​
     public void setRoles(List roles) {
         this.roles = roles;
     }
 }
  • Role

 package com.study.entity;
 ​
 /**
  * @ClassName Role
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/7 15:17
  * @Version 1.0
  */
 public class Role {
     private Integer id;
     private String name;
     private String nameZh;
 ​
     public Integer getId() {
         return id;
     }
 ​
     public void setId(Integer id) {
         this.id = id;
     }
 ​
     public String getName() {
         return name;
     }
 ​
     public void setName(String name) {
         this.name = name;
     }
 ​
     public String getNameZh() {
         return nameZh;
     }
 ​
     public void setNameZh(String nameZh) {
         this.nameZh = nameZh;
     }
 }

3.9.5 创建UserDao

 package com.study.dao;
 ​
 import com.study.entity.Role;
 import com.study.entity.User;
 import org.apache.ibatis.annotations.Mapper;
 ​
 import java.util.List;
 ​
 /**
  * @ClassName UserDao
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/7 15:19
  * @Version 1.0
  */
 @Mapper
 public interface UserDao {
     /**
      * 根据用户名查找用户
      *
      * @param username
      * @return
      */
     User loadUserByUsername(String username);
 ​
     /**
      * 根据用户id查询一个角色,注意一个用户可能不止一种角色
      *
      * @param uid
      * @return
      */
     List getRolesByUid(Integer uid);
 }

3.9.6 创建UserDAOMapper.xml

 
 
 ​
 
     
     
 ​
     
     
 

3.9.7 创建MyUserDetailsService

 package com.study.service;
 ​
 import com.study.dao.UserDao;
 import com.study.entity.User;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 import org.springframework.util.ObjectUtils;
 ​
 /**
  * @ClassName MyUserDetailsService
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/7 15:15
  * @Version 1.0
  */
 @Service
 public class MyUserDetailsService implements UserDetailsService {
     private final UserDao userDao;
 ​
     @Autowired
     public MyUserDetailsService(UserDao userDao) {
         this.userDao = userDao;
     }
 ​
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userDao.loadUserByUsername(username);
         if (ObjectUtils.isEmpty(user))
             throw new UsernameNotFoundException("用户不存在");
         user.setRoles(userDao.getRolesByUid(user.getId()));
         return user;
     }
 }

3.9.8 编写html

  • login.html

 
 
 
     
     登录页面
 
 
 

用户登录

 
    用户名:
    密码:
       
 

           
 

   
  • index.html

 
 
 
     
     系统主页
 
 
 

欢迎,进入我的系统!

 

用户详细信息如下:

 
         
  •      
  •      
  •      
  •      
  •  
 
 退出登录    

3.9.9 编写MvcConfigurer

 package com.study.config;
 ​
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 ​
 /**
  * @ClassName MvcConfigurer
  * @Description 对SpringMVC进行自定义配置
  * @Author Jiangnan Cui
  * @Date 2022/8/7 14:20
  * @Version 1.0
  */
 @Configuration
 public class MvcConfigurer implements WebMvcConfigurer {
 ​
     @Override
     public void addViewControllers(ViewControllerRegistry registry) {
         //addViewController: forward跳转
         registry.addViewController("/login.html").setViewName("login");
         registry.addViewController("/index.html").setViewName("index");
     }
 }

3.9.10 编写SecurityConfigurer

 package com.study.config;
 ​
 import com.study.service.MyUserDetailsService;
 import org.springframework.beans.factory.annotation.Autowired;
 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;
 ​
 /**
  * @ClassName SecurityConfiger
  * @Description 自定义security相关配置
  * @Author Jiangnan Cui
  * @Date 2022/8/7 14:35
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
     //    @Bean
 //    public UserDetailsService userDetailsService() {
 //        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
 //        userDetailsManager.createUser(User.withUsername("root").password("{noop}123456").roles("admin").build());
 //        return userDetailsManager;
 //    }
     @Autowired
     private MyUserDetailsService myUserDetailsService;
 ​
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(myUserDetailsService);
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .mvcMatchers("/login.html").permitAll()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin()
                 .loginPage("/login.html") //指定自定义登录页面
                 .loginProcessingUrl("/doLogin")
                 .usernameParameter("uname")
                 .passwordParameter("pwd")
                 .defaultSuccessUrl("/index.html", true)
                 .failureUrl("/login.html")
                 .and()
                 .logout()
                 .logoutUrl("/logout")
                 .logoutSuccessUrl("/login.html")
                 .and()
                 .csrf().disable();
     }
 }
 ​

3.9.11 启动服务测试 

(1)登录数据库中含有的用户时,登录成功后显示认证信息:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第17张图片

(2)登录数据库中不含有的用户时,认证后显示错误信息:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第18张图片

3.9.12 最终项目目录结构

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第19张图片

3.10 前后端分离开发认证总结案例

配套视频:28.前后端分离认证总结案例(一)_哔哩哔哩_bilibili

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第20张图片

3.10.1 新建项目

       新建module:spring-security-04split,勾选Spring Web,创建项目(注意:此处SpringBoot使用的是2.6.2版本),启动服务,查看服务是否能够正常启动!

3.10.2 新建Controller进行测试

 package com.study.controller;
 ​
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 /**
  * @ClassName TestController
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/9 16:11
  * @Version 1.0
  */
 @RestController
 public class TestController {
     @GetMapping("/test")
     public String test() {
         System.out.println("test is ok!");
         return "Test is Ok!";
     }
 }
 ​

       启动服务,浏览器访问:http://localhost:8080/test,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!

3.10.3 pom.xml引入依赖

 
 
     org.springframework.boot
     spring-boot-starter-security
 

       刷新maven,重启启动服务,浏览器再次访问:http://localhost:8080/test,此时需要进行登录认证,用户名为:user,密码为引入SpringSecurity框架后IDEA控制台产生的随机密码,输入正确的用户名和密码登录成功后,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!

3.10.4 编写SpringSecurity配置类

 package com.study.config;
 ​
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 ​
 /**
  * @ClassName SecurityConfig
  * @Description SpringSecurity配置类
  * @Author Jiangnan Cui
  * @Date 2022/8/9 17:36
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()//所有请求必须认证
                 .and()
                 .formLogin();//form表单认证登录
     }
 }
 ​

3.10.5 编写前后端分离认证Filter

       在进行前后端分离认证时,后端接收到的是前端发送过来的post形式的json数据,此时需要在json格式中获取用于登录的用户名和密码,而SpringSecurity中用于对用户名和密码进行拦截的过滤器为UsernamePasswordAuthenticationFilter,此时需要对此Filter进行覆盖,重写attemptAuthentication()方法后得到用户名和密码,从而进行认证。

 package com.study.config;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 ​
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.Map;
 ​
 /**
  * @ClassName LoginFilter
  * @Description 自定义前后端分离Filter
  * @Author Jiangnan Cui
  * @Date 2022/8/9 17:42
  * @Version 1.0
  */
 public class LoginFilter extends UsernamePasswordAuthenticationFilter {
     @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
         System.out.println("========================================");
         //1.判断是否是 post 方式请求
         if (!request.getMethod().equals("POST")) {
             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         }
         //2.判断是否是 json 格式请求类型
         if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
             //3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx"}
             try {
                 Map userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                 String username = userInfo.get(getUsernameParameter());
                 String password = userInfo.get(getPasswordParameter());
                 System.out.println("用户名: " + username + " 密码: " + password);
                 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                 setDetails(request, authRequest);
                 return this.getAuthenticationManager().authenticate(authRequest);
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         return super.attemptAuthentication(request, response);
     }
 }
 ​

3.10.6 完善SpringSecurity配置类

 package com.study.config;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
 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.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 ​
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @ClassName SecurityConfig
  * @Description SpringSecurity配置类
  * @Author Jiangnan Cui
  * @Date 2022/8/9 17:36
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
     //自定义AuthenticationManager
     @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
         inMemoryUserDetailsManager.createUser(
                 User.withUsername("root").password("{noop}123456").roles("admin").build());
         return inMemoryUserDetailsManager;
     }
 ​
     //覆盖默认的AuthenticationManager
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
     }
 ​
     //从工厂中暴露出来
     @Override
     @Bean
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 ​
     //把自定义Filter交给工厂,用以替换UsernamePasswordAuthenticationFilter
     @Bean
     public LoginFilter loginFilter() throws Exception {
         LoginFilter loginFilter = new LoginFilter();
         loginFilter.setFilterProcessesUrl("/doLogin");//指定认证url
         loginFilter.setUsernameParameter("uname");//指定接收json用户名key
         loginFilter.setPasswordParameter("pwd");//指定接收json密码key
         loginFilter.setAuthenticationManager(authenticationManagerBean());
         //认证成功处理
         loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
             @Override
             public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                 Map result = new HashMap();
                 result.put("msg", "登录成功");//打印登录成功信息
                 //result.put("status", 200);//打印状态码   此处改为setStatus
                 result.put("用户信息", authentication.getPrincipal());//获得身份信息
                 result.put("authentication", authentication);//打印认证信息
                 response.setContentType("application/json;charset=UTF-8");//设置响应类型
                 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
                 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
                 response.getWriter().println(s);//打印json格式数据
             }
         });
         //认证失败处理
         loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
             Map result = new HashMap();
             result.put("msg", "登录失败:" + exception.getMessage());
             //result.put("status", 500);//打印状态码   此处改为setStatus
             response.setContentType("application/json;charset=UTF-8");
             response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//设置登录失败之后的状态
             String s = new ObjectMapper().writeValueAsString(result);
             response.getWriter().println(s);
         });
         return loginFilter;
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()//所有请求必须认证
                 .and()
                 .formLogin()//form表单认证时默认采用UsernamePasswordAuthenticationFilter进行拦截
                 // 此处使用LoginFilter进行替换
                 .and()
                 .exceptionHandling()
                 .authenticationEntryPoint(new AuthenticationEntryPoint() {
                     @Override
                     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                         response.setContentType("application/json;charset=UTF-8");
                         response.setStatus(HttpStatus.UNAUTHORIZED.value());
                         response.getWriter().println("请认证之后再去处理!");
                     }
                 })
                 .and()
                 .logout()
                 //.logoutUrl("/logout")
                 .logoutRequestMatcher(new OrRequestMatcher(
                         new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
                         new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
                 ))
                 .logoutSuccessHandler((request, response, authentication) -> {
                     Map result = new HashMap();
                     result.put("msg", "注销成功");//打印登录成功信息
                     //result.put("status", 200);//打印状态码   此处改为setStatus
                     result.put("用户信息", authentication.getPrincipal());//获得身份信息
                     result.put("authentication", authentication);//打印认证信息
                     response.setContentType("application/json;charset=UTF-8");//设置响应类型
                     response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
                     String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
                     response.getWriter().println(s);//打印json格式数据
                 })
                 .and()
                 .csrf().disable();
         /**
          * addFilterAt:用某个filter替换掉过滤器链中的某个filter
          * addFilterBefore():将某个过滤器放在过滤器链中某个filter之前
          * addFilterAfter():将某个过滤器放在过滤器链中某个filter之后
          */
         http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
     }
 }

3.10.7 启动服务测试

启动服务,打开Postman进行测试:

(1)GET http://localhost:8080/test

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第21张图片

(2)POST http://localhost:8080/doLogin

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第22张图片

(3)GET或DELETE http://localhost:8080/logout

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第23张图片

3.10.8 引入数据源+整合mybatis

配套视频:29.前后端分离认证总结案例(二)_哔哩哔哩_bilibili

  • pom.xml引入依赖

 
 
     com.alibaba
     druid
     1.2.7
 
 ​
 
 
     mysql
     mysql-connector-java
     5.1.38
 
 ​
 
 
     org.mybatis.spring.boot
     mybatis-spring-boot-starter
     2.2.0
 

刷新maven!

  • application.properties添加配置

 # datasource:类型、驱动名、用户名、密码
 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
 spring.datasource.username=root
 spring.datasource.password=root
 # mybatis配置mapper文件的位置和别名设置
 # 注意mapper目录(包)新建时必须使用"/",而不是.
 mybatis.mapper-locations=classpath:com/study/mapper/*.xml
 mybatis.type-aliases-package=com.study.entity
 # log:为了显示mybatis运行SQL语句
 logging.level.com.study=debug
  • 创建实体类entity

    • User

       package com.study.entity;
       ​
       import org.springframework.security.core.GrantedAuthority;
       import org.springframework.security.core.authority.SimpleGrantedAuthority;
       import org.springframework.security.core.userdetails.UserDetails;
       ​
       import java.util.*;
       ​
       /**
        * @ClassName User
        * @Description TODO
        * @Author Jiangnan Cui
        * @Date 2022/8/10 0:25
        * @Version 1.0
        */
       public class User implements UserDetails {
           private Integer id;
           private String username;
           private String password;
           private Boolean enabled;
           private Boolean accountNonExpired;
           private Boolean accountNonLocked;
           private Boolean credentialsNonExpired;
           private List roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息
       ​
           //返回权限信息
           @Override
           public Collection getAuthorities() {
               Set authorities = new HashSet<>();
               roles.forEach(role -> {
                   SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
                   authorities.add(simpleGrantedAuthority);
               });
               return authorities;
           }
       ​
           @Override
           public String getPassword() {
               return password;
           }
       ​
           public void setPassword(String password) {
               this.password = password;
           }
       ​
           @Override
           public String getUsername() {
               return username;
           }
       ​
           public void setUsername(String username) {
               this.username = username;
           }
       ​
           @Override
           public boolean isAccountNonExpired() {
               return accountNonLocked;
           }
       ​
           @Override
           public boolean isAccountNonLocked() {
               return accountNonLocked;
           }
       ​
           @Override
           public boolean isCredentialsNonExpired() {
               return credentialsNonExpired;
           }
       ​
           @Override
           public boolean isEnabled() {
               return enabled;
           }
       ​
           public Integer getId() {
               return id;
           }
       ​
           public void setId(Integer id) {
               this.id = id;
           }
       ​
           public Boolean getEnabled() {
               return enabled;
           }
       ​
           public void setEnabled(Boolean enabled) {
               this.enabled = enabled;
           }
       ​
           public Boolean getAccountNonExpired() {
               return accountNonExpired;
           }
       ​
           public void setAccountNonExpired(Boolean accountNonExpired) {
               this.accountNonExpired = accountNonExpired;
           }
       ​
           public Boolean getAccountNonLocked() {
               return accountNonLocked;
           }
       ​
           public void setAccountNonLocked(Boolean accountNonLocked) {
               this.accountNonLocked = accountNonLocked;
           }
       ​
           public Boolean getCredentialsNonExpired() {
               return credentialsNonExpired;
           }
       ​
           public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
               this.credentialsNonExpired = credentialsNonExpired;
           }
       ​
           public List getRoles() {
               return roles;
           }
       ​
           public void setRoles(List roles) {
               this.roles = roles;
           }
       }
    • Role

       package com.study.entity;
       ​
       /**
        * @ClassName Role
        * @Description TODO
        * @Author Jiangnan Cui
        * @Date 2022/8/10 0:25
        * @Version 1.0
        */
       public class Role {
           private Integer id;
           private String name;
           private String nameZh;
       ​
           public Integer getId() {
               return id;
           }
       ​
           public void setId(Integer id) {
               this.id = id;
           }
       ​
           public String getName() {
               return name;
           }
       ​
           public void setName(String name) {
               this.name = name;
           }
       ​
           public String getNameZh() {
               return nameZh;
           }
       ​
           public void setNameZh(String nameZh) {
               this.nameZh = nameZh;
           }
       }
  • 创建UserDao

 package com.study.dao;
 ​
 import com.study.entity.Role;
 import com.study.entity.User;
 import org.apache.ibatis.annotations.Mapper;
 ​
 import java.util.List;
 ​
 /**
  * @ClassName UserDao
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/10 0:27
  * @Version 1.0
  */
 @Mapper
 public interface UserDao {
     /**
      * 根据用户名查找用户
      *
      * @param username
      * @return
      */
     User loadUserByUsername(String username);
 ​
     /**
      * 根据用户id查询一个角色,注意一个用户可能不止一种角色
      *
      * @param uid
      * @return
      */
     List getRolesByUid(Integer uid);
 }
  • 创建UserDaoMapper.xml

 
 
 ​
 
     
     
 ​
     
     
 
  • 创建MyUserDetailsService

 package com.study.service;
 ​
 import com.study.dao.UserDao;
 import com.study.entity.User;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 import org.springframework.util.ObjectUtils;
 ​
 /**
  * @ClassName MyUserDetailsService
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/10 0:26
  * @Version 1.0
  */
 @Service
 public class MyUserDetailsService implements UserDetailsService {
     private final UserDao userDao;
 ​
     @Autowired
     public MyUserDetailsService(UserDao userDao) {
         this.userDao = userDao;
     }
 ​
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userDao.loadUserByUsername(username);
         if (ObjectUtils.isEmpty(user))
             throw new UsernameNotFoundException("用户不存在");
         user.setRoles(userDao.getRolesByUid(user.getId()));
         return user;
     }
 }
  • SecurityConfig修改数据源

 package com.study.config;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.study.service.MyUserDetailsService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
 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.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 ​
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @ClassName SecurityConfig
  * @Description SpringSecurity配置类
  * @Author Jiangnan Cui
  * @Date 2022/8/9 17:36
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
     //自定义AuthenticationManager
 //    @Bean
 //    public UserDetailsService userDetailsService() {
 //        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
 //        inMemoryUserDetailsManager.createUser(
 //                User.withUsername("root").password("{noop}123456").roles("admin").build());
 //        return inMemoryUserDetailsManager;
 //    }
 ​
     //引入数据源
     private MyUserDetailsService myUserDetailsService;
 ​
     @Autowired
     public SecurityConfig(MyUserDetailsService myUserDetailsService) {
         this.myUserDetailsService = myUserDetailsService;
     }
 ​
     //覆盖默认的AuthenticationManager
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         //auth.userDetailsService(userDetailsService());
         auth.userDetailsService(myUserDetailsService);
     }
 ​
     //从工厂中暴露出来
     @Override
     @Bean
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 ​
     //把自定义Filter交给工厂,用以替换UsernamePasswordAuthenticationFilter
     @Bean
     public LoginFilter loginFilter() throws Exception {
         LoginFilter loginFilter = new LoginFilter();
         loginFilter.setFilterProcessesUrl("/doLogin");//指定认证url
         loginFilter.setUsernameParameter("uname");//指定接收json用户名key
         loginFilter.setPasswordParameter("pwd");//指定接收json密码key
         loginFilter.setAuthenticationManager(authenticationManagerBean());
         //认证成功处理
         loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
             @Override
             public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                 Map result = new HashMap();
                 result.put("msg", "登录成功");//打印登录成功信息
                 //result.put("status", 200);//打印状态码   此处改为setStatus
                 result.put("用户信息", authentication.getPrincipal());//获得身份信息
                 result.put("authentication", authentication);//打印认证信息
                 response.setContentType("application/json;charset=UTF-8");//设置响应类型
                 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
                 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
                 response.getWriter().println(s);//打印json格式数据
             }
         });
         //认证失败处理
         loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
             Map result = new HashMap();
             result.put("msg", "登录失败:" + exception.getMessage());
             //result.put("status", 500);//打印状态码   此处改为setStatus
             response.setContentType("application/json;charset=UTF-8");
             response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//设置登录失败之后的状态
             String s = new ObjectMapper().writeValueAsString(result);
             response.getWriter().println(s);
         });
         return loginFilter;
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()//所有请求必须认证
                 .and()
                 .formLogin()//form表单认证时默认采用UsernamePasswordAuthenticationFilter进行拦截
                 // 此处使用LoginFilter进行替换
                 .and()
                 .exceptionHandling()
                 .authenticationEntryPoint(new AuthenticationEntryPoint() {
                     @Override
                     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                         response.setContentType("application/json;charset=UTF-8");
                         response.setStatus(HttpStatus.UNAUTHORIZED.value());
                         response.getWriter().println("请认证之后再去处理!");
                     }
                 })
                 .and()
                 .logout()
                 //.logoutUrl("/logout")
                 .logoutRequestMatcher(new OrRequestMatcher(
                         new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
                         new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
                 ))
                 .logoutSuccessHandler((request, response, authentication) -> {
                     Map result = new HashMap();
                     result.put("msg", "注销成功");//打印登录成功信息
                     //result.put("status", 200);//打印状态码   此处改为setStatus
                     result.put("用户信息", authentication.getPrincipal());//获得身份信息
                     result.put("authentication", authentication);//打印认证信息
                     response.setContentType("application/json;charset=UTF-8");//设置响应类型
                     response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
                     String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
                     response.getWriter().println(s);//打印json格式数据
                 })
                 .and()
                 .csrf().disable();
         /**
          * addFilterAt:用某个filter替换掉过滤器链中的某个filter
          * addFilterBefore():将某个过滤器放在过滤器链中某个filter之前
          * addFilterAfter():将某个过滤器放在过滤器链中某个filter之后
          */
         http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
     }
 }

3.10.9 启动服务测试

启动服务,打开Postman进行测试:

(1)GET http://localhost:8080/test 登录失败

(2)POST http://localhost:8080/doLogin

uname:root,pwd:123

uname:blr,pwd:123

uname:admin,pwd:123

均可成功登录

(3)GET或DELETE http://localhost:8080/logout 成功注销

3.10.10 最终项目结构目录

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第24张图片

3.11 传统Web开发认证验证码

配套视频:30.传统 web 开发之添加验证码(一)_哔哩哔哩_bilibili

配套视频:31.传统 web 开发之添加验证码(二)_哔哩哔哩_bilibili

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第25张图片

3.11.1 新建项目

       新建module:spring-security-05,勾选Spring Web,创建项目(注意:此处SpringBoot使用的是2.6.2版本),启动服务,查看服务是否能够正常启动!

3.11.2 pom.xml引入依赖

 
 
     org.springframework.boot
     spring-boot-starter-thymeleaf
 
 ​
 
 
     org.springframework.boot
     spring-boot-starter-security
 

刷新Maven!

3.11.3 application.properties添加配置

 spring.thymeleaf.cache=false
 spring.thymeleaf.prefix=classpath:/templates/
 spring.thymeleaf.suffix=.html

3.11.4 创建html页面

  • login.html

 
 
 
     
     登录页面
 
 
 

用户登录

 
    用户名:
    密码:
       
   
  • index.html

 
 
 
     
     系统主页
 
 
 

欢迎来到小崔的主页!

   

3.11.5 配置MvcConfigurer

 package com.study.config;
 ​
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 ​
 /**
  * @ClassName MvcConfigurer
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 12:14
  * @Version 1.0
  */
 @Configuration
 public class MvcConfigurer implements WebMvcConfigurer {
     @Override
     public void addViewControllers(ViewControllerRegistry registry) {
         registry.addViewController("/index.html").setViewName("index");
         registry.addViewController("/login.html").setViewName("login");
     }
 }
 ​

3.11.6 配置Security

 package com.study.config;
 ​
 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.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 ​
 /**
  * @ClassName SecurityConfigurer
  * @Description 配置Security
  * @Author Jiangnan Cui
  * @Date 2022/8/11 12:26
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
         inMemoryUserDetailsManager.createUser(
                 User.withUsername("root").
                         password("{noop}123456").
                         roles("admin").
                         build());
         return inMemoryUserDetailsManager;
     }
 ​
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .mvcMatchers("/login.html").permitAll()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin()
                 .loginPage("/login.html")
                 .loginProcessingUrl("/doLogin")
                 .usernameParameter("uname")
                 .passwordParameter("pwd")
                 .defaultSuccessUrl("/index.html", true)
                 .failureUrl("/login.html")
                 .and()
                 .logout()
                 .logoutUrl("/logout")
                 .logoutSuccessUrl("/login.html")
                 .and()
                 .csrf().disable();
     }
 }

3.11.7 启动服务测试

       启动服务,浏览器访问:http://localhost:8080/index.html,此时需要进行登录认证,用户名为root,密码为123456,输入后正常访问index.html,表示测试成功!注销登录访问路径:http://localhost:8080/login.html。

3.11.8 配置验证码

  • pom.xml引入依赖kaptcha

 
 
     com.github.penggle
     kaptcha
     2.3.2
 
  • 添加验证码配置

 package com.study.config;
 ​
 import com.google.code.kaptcha.Producer;
 import com.google.code.kaptcha.impl.DefaultKaptcha;
 import com.google.code.kaptcha.util.Config;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 ​
 import java.util.Properties;
 ​
 /**
  * @ClassName KaptchaConfigurer
  * @Description 生成验证码
  * @Author Jiangnan Cui
  * @Date 2022/8/11 16:16
  * @Version 1.0
  */
 @Configuration
 public class KaptchaConfigurer {
     @Bean
     public Producer kaptcha() {
         Properties properties = new Properties();
         //1.验证码宽度
         properties.setProperty("kaptcha.image.width", "150");
         //2.验证码高度
         properties.setProperty("kaptcha.image.height", "50");
         //3.验证码字符串
         properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
         //4.验证码长度
         properties.setProperty("kaptcha.textproducer.char.length", "4");
         Config config = new Config(properties);
         DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
         defaultKaptcha.setConfig(config);
         return defaultKaptcha;
     }
 }

3.11.9 在login.html中添加验证码

 
 
 
     
     登录页面
 
 
 

用户登录

 
    用户名:
    密码:
    验证码:        
   

并在SecurityConfigurer对 http://localhost:8080/vc.jpg 请求进行放行。

3.11.10 生成验证码Controller

 package com.study.controller;
 ​
 import com.google.code.kaptcha.Producer;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.RequestMapping;
 ​
 import javax.imageio.ImageIO;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
 ​
 /**
  * @ClassName VerifyCodeController
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 16:20
  * @Version 1.0
  */
 @Controller
 public class VerifyCodeController {
     private final Producer producer;
 ​
     @Autowired
     public VerifyCodeController(Producer producer) {
         this.producer = producer;
     }
 ​
     @RequestMapping("/vc.jpg")
     public void verityCode(HttpServletResponse response, HttpSession session) throws IOException {
         //1.生成验证码
         String verifyCode = producer.createText();
         //2.保存到session中
         session.setAttribute("kaptcha", verifyCode);
         //3.生成图片
         BufferedImage bi = producer.createImage(verifyCode);
         //4.设置响应类型,并响应成图片
         response.setContentType("image/png");
         ServletOutputStream os = response.getOutputStream();
         ImageIO.write(bi, "jpg", os);
     }
 }

3.11.11 测试

启动服务,访问 http://localhost:8080/vc.jpg 可得到随机生成的四位验证码图片:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第26张图片

访问 http://localhost:8080/vc.jpg 可发现登录页出现验证码:

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第27张图片

3.11.12 自定义filter验证验证码

 package com.study.filter;
 ​
 import com.study.exception.KaptchaNotMatchException;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.util.ObjectUtils;
 ​
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 ​
 /**
  * @ClassName KaptchaFilter
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 17:29
  * @Version 1.0
  */
 public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
     public static final String KAPTCHA_KEY = "kaptcha";
     private String kaptcha = KAPTCHA_KEY;
 ​
     public String getKaptcha() {
         return kaptcha;
     }
 ​
     public void setKaptcha(String kaptcha) {
         this.kaptcha = kaptcha;
     }
 ​
     @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
         //1.判断是否是post方式
         if (request.getMethod().equals("POST")) {
             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         }
         //2.从请求中获取验证码
         String verifyCode = request.getParameter(getKaptcha());
         //与session中的验证码进行比较
         String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
         if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode) && verifyCode.equalsIgnoreCase(sessionVerifyCode)) {
             return super.attemptAuthentication(request, response);
         }
         //不满足条件时直接抛出异常
         throw new KaptchaNotMatchException("验证码不匹配");
     }
 }
 ​

3.11.13 自定义验证码异常类

 package com.study.exception;
 import org.springframework.security.core.AuthenticationException;
 ​
 /**
  * @ClassName KaptchaNotMatchException
  * @Description 自定义验证码认证异常
  * @Author Jiangnan Cui
  * @Date 2022/8/11 17:39
  * @Version 1.0
  */
 public class KaptchaNotMatchException extends AuthenticationException {
 ​
     public KaptchaNotMatchException(String msg) {
         super(msg);
     }
 ​
     public KaptchaNotMatchException(String msg, Throwable cause) {
         super(msg, cause);
     }
 }

3.11.14 SecurityConfigurer配置验证码filter

 package com.study.config;
 ​
 import com.study.filter.KaptchaFilter;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.authentication.AuthenticationManager;
 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.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 ​
 /**
  * @ClassName SecurityConfigurer
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 12:26
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
     
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
         inMemoryUserDetailsManager.createUser(
                 User.withUsername("root").
                         password("{noop}123456").
                         roles("admin").
                         build());
         return inMemoryUserDetailsManager;
     }
 ​
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
     }
 ​
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 ​
     @Bean
     public KaptchaFilter kaptchaFilter() throws Exception {
         KaptchaFilter kaptchaFilter = new KaptchaFilter();
         //指定接收验证码请求参数名
         kaptchaFilter.setKaptcha("kaptcha");
         //指定认证管理器
         kaptchaFilter.setAuthenticationManager(authenticationManagerBean());
         //指定认证成功处理
         kaptchaFilter.setAuthenticationSuccessHandler((res, resp, auth) -> {
             resp.sendRedirect("/index.html");
         });
         //指定认证失败处理
         kaptchaFilter.setAuthenticationFailureHandler((res, resp, auth) -> {
             resp.sendRedirect("/login.html");
         });
         return kaptchaFilter;
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .mvcMatchers("/login.html").permitAll()
                 .mvcMatchers("/vc.jpg").permitAll()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin()
                 .loginPage("/login.html")
                 .loginProcessingUrl("/doLogin")
                 .usernameParameter("uname")
                 .passwordParameter("pwd")
 //                .defaultSuccessUrl("/index.html", true)
 //                .failureUrl("/login.html")
                 .and()
                 .logout()
                 .logoutUrl("/logout")
                 .logoutSuccessUrl("/login.html")
                 .and()
                 .csrf().disable();
         http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
     }
 }
 ​

3.11.15 测试

       启动服务,访问 http://localhost:8080/login.html ,输入正确的用户名、密码、验证码后可正常登录,表示测试成功!

3.11.16 项目最终目录结构

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第28张图片

3.12 前后端分离开发

配套视频:32.前后端分离开发之添加验证码_哔哩哔哩_bilibili

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第29张图片

3.12.1 新建项目

       新建module:spring-security-06,勾选Spring Web、Spring Security,创建项目(注意:此处SpringBoot使用的是2.6.2版本),启动服务,查看服务是否能够正常启动!

3.12.2 pom.xml引入依赖

 
         
             com.github.penggle
             kaptcha
             2.3.2
         
 ​
         
         
             com.alibaba
             druid
             1.2.7
         
 ​
         
         
             mysql
             mysql-connector-java
             5.1.38
         
 ​
         
         
             org.mybatis.spring.boot
             mybatis-spring-boot-starter
             2.2.0
         

刷新Maven!

3.12.3 application.properties添加配置

 # datasource:类型、驱动名、url、用户名、密码
 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
 spring.datasource.username=root
 spring.datasource.password=root
 # mybatis配置mapper文件的位置和别名设置
 # 注意mapper目录(包)新建时必须使用"/",而不是.
 mybatis.mapper-locations=classpath:com/study/mapper/*.xml
 mybatis.type-aliases-package=com.study.entity
 # log:为了显示mybatis运行SQL语句
 logging.level.com.study=debug

3.12.4 创建实体类

  • User

 package com.study.entity;
 ​
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 ​
 import java.util.*;
 ​
 /**
  * @ClassName User
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 22:58
  * @Version 1.0
  */
 public class User implements UserDetails {
     private Integer id;
     private String username;
     private String password;
     private Boolean enabled;
     private Boolean accountNonExpired;
     private Boolean accountNonLocked;
     private Boolean credentialsNonExpired;
     private List roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息
 ​
     //返回权限信息
     @Override
     public Collection getAuthorities() {
         Set authorities = new HashSet<>();
         roles.forEach(role -> {
             SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
             authorities.add(simpleGrantedAuthority);
         });
         return authorities;
     }
 ​
     @Override
     public String getPassword() {
         return password;
     }
 ​
     public void setPassword(String password) {
         this.password = password;
     }
 ​
     @Override
     public String getUsername() {
         return username;
     }
 ​
     public void setUsername(String username) {
         this.username = username;
     }
 ​
     @Override
     public boolean isAccountNonExpired() {
         return accountNonLocked;
     }
 ​
     @Override
     public boolean isAccountNonLocked() {
         return accountNonLocked;
     }
 ​
     @Override
     public boolean isCredentialsNonExpired() {
         return credentialsNonExpired;
     }
 ​
     @Override
     public boolean isEnabled() {
         return enabled;
     }
 ​
     public Integer getId() {
         return id;
     }
 ​
     public void setId(Integer id) {
         this.id = id;
     }
 ​
     public Boolean getEnabled() {
         return enabled;
     }
 ​
     public void setEnabled(Boolean enabled) {
         this.enabled = enabled;
     }
 ​
     public Boolean getAccountNonExpired() {
         return accountNonExpired;
     }
 ​
     public void setAccountNonExpired(Boolean accountNonExpired) {
         this.accountNonExpired = accountNonExpired;
     }
 ​
     public Boolean getAccountNonLocked() {
         return accountNonLocked;
     }
 ​
     public void setAccountNonLocked(Boolean accountNonLocked) {
         this.accountNonLocked = accountNonLocked;
     }
 ​
     public Boolean getCredentialsNonExpired() {
         return credentialsNonExpired;
     }
 ​
     public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
         this.credentialsNonExpired = credentialsNonExpired;
     }
 ​
     public List getRoles() {
         return roles;
     }
 ​
     public void setRoles(List roles) {
         this.roles = roles;
     }
 }
  • Role

 package com.study.entity;
 ​
 /**
  * @ClassName Role
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 22:59
  * @Version 1.0
  */
 public class Role {
     private Integer id;
     private String name;
     private String nameZh;
 ​
     public Integer getId() {
         return id;
     }
 ​
     public void setId(Integer id) {
         this.id = id;
     }
 ​
     public String getName() {
         return name;
     }
 ​
     public void setName(String name) {
         this.name = name;
     }
 ​
     public String getNameZh() {
         return nameZh;
     }
 ​
     public void setNameZh(String nameZh) {
         this.nameZh = nameZh;
     }
 }

3.12.4 创建UserDao

 package com.study.dao;
 ​
 import com.study.entity.Role;
 import com.study.entity.User;
 import org.apache.ibatis.annotations.Mapper;
 ​
 import java.util.List;
 ​
 /**
  * @ClassName UserDao
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 22:59
  * @Version 1.0
  */
 @Mapper
 public interface UserDao {
     User loadUserByUsername(String username);
 ​
     /**
      * 根据用户id查询一个角色,注意一个用户可能不止一种角色
      *
      * @param uid
      * @return
      */
     List getRolesByUid(Integer uid);
 }

3.12.5 创建UserDaoMapper

 
 
 ​
 
     
     
 ​
     
     
 

3.12.6 创建MyUserDetailsService

 package com.study.service;
 ​
 import com.study.dao.UserDao;
 import com.study.entity.User;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 import org.springframework.util.ObjectUtils;
 ​
 /**
  * @ClassName MyUserDetailsService
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 23:02
  * @Version 1.0
  */
 @Service
 public class MyUserDetailsService implements UserDetailsService {
     private final UserDao userDao;
 ​
     @Autowired
     public MyUserDetailsService(UserDao userDao) {
         this.userDao = userDao;
     }
 ​
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userDao.loadUserByUsername(username);
         if (ObjectUtils.isEmpty(user))
             throw new UsernameNotFoundException("用户不存在");
         user.setRoles(userDao.getRolesByUid(user.getId()));
         return user;
     }
 }

3.12.7 验证码

  • KaptchaConfigurer 配置

 package com.study.config;
 ​
 import com.google.code.kaptcha.Producer;
 import com.google.code.kaptcha.impl.DefaultKaptcha;
 import com.google.code.kaptcha.util.Config;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 ​
 import java.util.Properties;
 ​
 /**
  * @ClassName KaptchaConfigurer
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 23:04
  * @Version 1.0
  */
 @Configuration
 public class KaptchaConfigurer {
     @Bean
     public Producer kaptcha() {
         Properties properties = new Properties();
         //1.验证码宽度
         properties.setProperty("kaptcha.image.width", "150");
         //2.验证码高度
         properties.setProperty("kaptcha.image.height", "50");
         //3.验证码字符串
         properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
         //4.验证码长度
         properties.setProperty("kaptcha.textproducer.char.length", "4");
         Config config = new Config(properties);
         DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
         defaultKaptcha.setConfig(config);
         return defaultKaptcha;
     }
 }
 ​
  • VerityCodeController 控制器

 package com.study.controller;
 ​
 import com.google.code.kaptcha.Producer;
 import org.apache.tomcat.util.codec.binary.Base64;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.FastByteArrayOutputStream;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 import javax.imageio.ImageIO;
 import javax.servlet.http.HttpSession;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
 ​
 /**
  * @ClassName VerifyCodeController
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 23:06
  * @Version 1.0
  */
 @RestController //一定是RestController而非Controller
 public class VerifyCodeController {
     private final Producer producer;
 ​
     @Autowired
     public VerifyCodeController(Producer producer) {
         this.producer = producer;
     }
 ​
     @GetMapping("/vc.jpg")
     public String getVerifyCode(HttpSession session) throws IOException {
         //1.生成验证码
         String verifyCode = producer.createText();
         //2.存入Session或Redis
         session.setAttribute("kaptcha", verifyCode);
         //3.生成图片
         BufferedImage bi = producer.createImage(verifyCode);
         //图片转化为字节
         FastByteArrayOutputStream fbaos = new FastByteArrayOutputStream();
         ImageIO.write(bi, "jpg", fbaos);
         //4.返回base64
         return Base64.encodeBase64String(fbaos.toByteArray());
     }
 }
 ​
  • LoginKaptchaFilter 过滤器

 package com.study.filter;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.study.exception.KaptchaNotMatchException;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.util.ObjectUtils;
 ​
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.Map;
 ​
 /**
  * @ClassName LoginKaptchaFilter
  * @Description 自定义Filter
  * @Author Jiangnan Cui
  * @Date 2022/8/11 23:17
  * @Version 1.0
  */
 public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
     public static final String FORM_KAPTCHA_KEY = "kaptcha";
     private String kaptchaParameter = FORM_KAPTCHA_KEY;
 ​
     public String getKaptchaParameter() {
         return kaptchaParameter;
     }
 ​
     public void setKaptchaParameter(String kaptchaParameter) {
         this.kaptchaParameter = kaptchaParameter;
     }
 ​
     @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
         //1.判断是否是POST请求
         if (!request.getMethod().equals("POST")) {
             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         }
         try {
             //2.获取请求数据
             Map userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
             String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码
             String username = userInfo.get(getUsernameParameter());//用来接收用户名
             String password = userInfo.get(getPasswordParameter());//用来接收密码
             //3.获取session中验证码
             String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");
             if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&
                     kaptcha.equalsIgnoreCase(sessionVerifyCode)) {
                 //4.获取用户名和密码认证
                 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                 setDetails(request, authRequest);
                 return this.getAuthenticationManager().authenticate(authRequest);
             }
         } catch (IOException e) {
             e.printStackTrace();
         }
         throw new KaptchaNotMatchException("验证码不匹配");
     }
 }
 ​
  • 定义验证码异常类

     public class KaptchaNotMatchException extends AuthenticationException {
     ​
         public KaptchaNotMatchException(String msg) {
             super(msg);
         }
     ​
         public KaptchaNotMatchException(String msg, Throwable cause) {
             super(msg, cause);
         }
     }

3.12.8 Security配置

 package com.study.config;
 ​
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.study.filter.LoginKaptchaFilter;
 import com.study.service.MyUserDetailsService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
 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.web.authentication.UsernamePasswordAuthenticationFilter;
 ​
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @ClassName SecurityConfig
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/11 23:06
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 ​
     //自定义内存数据源
     private MyUserDetailsService myUserDetailsService;
 ​
     @Autowired
     public SecurityConfig(MyUserDetailsService myUserDetailsService) {
         this.myUserDetailsService = myUserDetailsService;
     }
 ​
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(myUserDetailsService);
     }
 ​
     @Override
     @Bean
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 ​
     //配置
     @Bean
     public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
         LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();
         //1.认证 url
         loginKaptchaFilter.setFilterProcessesUrl("/doLogin");
         //2.认证 接收参数
         loginKaptchaFilter.setUsernameParameter("uname");
         loginKaptchaFilter.setPasswordParameter("passwd");
         loginKaptchaFilter.setKaptchaParameter("kaptcha");
         //3.指定认证管理器
         loginKaptchaFilter.setAuthenticationManager(authenticationManagerBean());
         //4.指定成功时处理
         loginKaptchaFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
             Map result = new HashMap();
             result.put("msg", "登录成功");
             result.put("用户信息", authentication.getPrincipal());
             resp.setContentType("application/json;charset=UTF-8");
             resp.setStatus(HttpStatus.OK.value());
             String s = new ObjectMapper().writeValueAsString(result);
             resp.getWriter().println(s);
         });
         //5.认证失败处理
         loginKaptchaFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
             Map result = new HashMap();
             result.put("msg", "登录失败: " + ex.getMessage());
             resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
             resp.setContentType("application/json;charset=UTF-8");
             String s = new ObjectMapper().writeValueAsString(result);
             resp.getWriter().println(s);
         });
         return loginKaptchaFilter;
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .mvcMatchers("/vc.jpg").permitAll()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin()
                 .and()
                 .exceptionHandling()
                 .authenticationEntryPoint((req, resp, ex) -> {
                     resp.setContentType("application/json;charset=UTF-8");
                     resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                     resp.getWriter().println("必须认证之后才能访问!");
                 })
                 .and()
                 .logout()
                 .and()
                 .csrf().disable();
 ​
         http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
     }
 }
 ​

3.12.9 测试

启动服务,打开postman,测试过程如下:

(1)生成验证码:http://localhost:8080/vc.jpg

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第30张图片

       破解base64格式的验证码:图片Base64编码互转工具-在线工具,复制上面的数据到输入框内,前面加上前缀:data:image/png;base64,得到对应的4位验证码数字。

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第31张图片

(2)测试登录:http://localhost:8080/doLogin,测试数据均为数据库已有数据

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第32张图片

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第33张图片

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第34张图片

 3.12.10 项目最终目录结构

【编程不良人】SpringSecurity实战学习笔记02---自定义认证_第35张图片

你可能感兴趣的:(Spring,springboot)