自定义用户认证流程

本小节介绍用户自定义认证流程,主要包括自定义登录页面,优化自定义登录页面,以及登录成功或者登录失败之后的处理。在项目中,开发人员一般都会有自己的处理方式,可结合部分内容使得更符合自己的项目场景。

自定义登录页面

在 Security 中,默认的登录页面是固定的,但在实际开发中,多半是不符合的,因此需要使用自己的登录页面,在 Security 中支持自定义登录页面。原本的登录页面的源码如下所示。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   private boolean postOnly = true;
   public UsernamePasswordAuthenticationFilter() {
      super(new AntPathRequestMatcher("/login", "POST"));
   }
}
java

在上文的代码中,可以看到过滤器上的登录只能是 POST 的 login。自定义登录页面,首先设置配置文件,代码如下所示。

package com.cao.security.browser;
/**
  * 覆盖掉security原有的配置
  * @author dell
  *
  */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
   @Override
   protected void configure(HttpSecurity http) throws Exception {
      //表单登录的一个安全认证环境
      http.formLogin()
          .loginPage("/index.html")
          .loginProcessingUrl("/authentication/form")
            // http.httpBasic()
          .and()
          .authorizeRequests() //请求授权
          .antMatchers("/index.html").permitAll() //这个URL不需要认证
          .anyRequest() //任何请求
          .authenticated() //都需要认证
          .and()
          .csrf().disable(); //去掉csrf的防护
   }

   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}
java

在上面代码中,定义了登录的页面是 index.html,登录的方法是 authentication/form。这里需要注意的是,index 也是需要通过过滤器链的,但在实际中这个请求不存在用户名与密码,不需要进行校验。所以在请求的校验时,有 index 就不需要进行校验。然后,写一个登录的页面的程序,代码如下所示。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>用户名</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登录</button></td>
        </tr>
    </table>
</form>
</body>
</html>
html

在上面的代码中,只写用户名与密码,用于演示校验。这里需要注意的是,登录的方法需要和配置文件中的程序保持一致。运行程序,然后访问链接,就可以进行登录认证,登录页面如图9.8所示。

image 2024 03 31 23 58 31 528
Figure 1. 图9.8 自定义登录页面

优化自定义登录页面

通过设置配置文件,我们已经实现自定义登录页面,但是这只是初步的实现,我们还需要对其进行优化。在上文的实现中,采取的是页面直接跳转的方式,而现在我们需要通过控制器进行统一管理,所以,就需要按照这个思路进行优化。在优化之前,先看看优化的思路图,如图9.9所示。

image 2024 03 31 23 59 00 340
Figure 2. 图9.9 优化的思路图

首先,修改配置文件,代码如下所示。

package com.cao.security.browser;
/**
  * 覆盖security原有的配置
  * @author dell
  *
  */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
   @Autowired
   private SecurityProperties securityProperties;
   @Override
   protected void configure(HttpSecurity http) throws Exception {
      //表单登录的一个安全认证环境
      http.formLogin()
          .loginPage("/authentication/require")
          .loginProcessingUrl("/authentication/form")
      http.httpBasic()
          .and()
          .authorizeRequests() //请求授权
          .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll() //这个URL不需要认证,包含自定义的登录页
          .anyRequest() //任何请求
          .authenticated() //都需要认证
          .and()
          .csrf().disable(); //去掉csrf的防护
   }
   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}
java

在上面的配置文件代码中,可以发现没有 index.html 的相关内容,只有 authentication/require 的请求。

在上面的代码中,有 securityProperties 的这一段代码定义的 URL 不需要经过统一认证。对于这里的处理,我们还是通过抽象一些方法来实现,图9.10是抽象的一个框架。

image 2024 04 01 00 00 12 696
Figure 3. 图9.10 系统抽象封装

在9.10图中,最外层是 SecurityProperties,然后里面的对象分别是浏览器端与移动端的对象,分别用于做配置项。对于 SecurityProperties,代码如下所示。

package com.cao.security.core.properties;
@ConfigurationProperties(prefix="jun.security")
public class SecurityProperties {
   private BrowserProperties browser=new BrowserProperties();
   public BrowserProperties getBrowser() {
      return browser;
   }
   public void setBrowser(BrowserProperties browser) {
      this.browser = browser;
   }
}
java

在上面的程序中,我们先写一个 BrowserProperties 对象。这里会读取 jun.security 开头的配置项,同时会把配置项的第三个字段读取到同 Browser 相同的类中,所以后续还要写 Browser 的类。

现在开始写 BrowserProperties 对象,这里要读取的是配置项的第四个字段。loginPage 里有一个默认值,如果用户没有指定,就使用初始化的值,代码如下所示。

package com.cao.security.core.properties;
public class BrowserProperties {
   private String loginPage="/index.html";
   public String getLoginPage() {
      return loginPage;
   }
   public void setLoginPage(String loginPage) {
      this.loginPage = loginPage;
   }
}
java

然后,为了让配置项可以生效,还需要写一个配置类,代码如下所示。

package com.cao.security.core.properties;
@Configuration
//让SecurityProperties读取器生效
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
java

现在,需要控制类处理请求,请求的代码如下所示。

package com.cao.security.browser;
@RestController
public class BrowserSecurityController {
   private Logger logger=LoggerFactory.getLogger(getClass());
   //拿到引发身份跳转的请求
   //因为在跳转之前,security会将请求缓存到session中
   private RequestCache requestCache=new HttpSessionRequestCache();
   //跳转
   private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();
   //方便读取自定义登录页的配置项
   @Autowired
   private SecurityProperties securityProperties;
   /**
   * 当需要身份认证的时候,跳转到这里
   * @param request
   * @param response
   * @return
   * @throws Exception
   */
   @RequestMapping("/authentication/require")
   @ResponseStatus(code=HttpStatus.UNAUTHORIZED)
   public SimpleResponse requiredAuthentication(HttpServletRequest request,HttpServletResponse response) throws Exception {
      SavedRequest saveRequest=requestCache.getRequest(request, response);
      if(saveRequest!=null) {
          String target=saveRequest.getRedirectUrl();
          logger.info("引发跳转的请求:"+target);
          if(StringUtils.endsWithIgnoreCase(target, ".html")) {
             //跳转到一个自定义的登录页
             redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
          }
      }
      return new SimpleResponse("访问的服务需要身份认证,请引导到登录页");
   }
}
java

在控制类中,在返回状态码的时候,需要使用一个对象,这个对象是 SimpleResonse,这个类的代码如下所示。

package com.cao.security.browser.support;
public class SimpleResponse {
   public SimpleResponse(Object content) {
      this.content=content;
   }
   private Object content;
ect getContent() {
      return content;
   }
   public void setContent(Object content) {
      this.content = content;
   }
}
java

配置项如下所示。

#JDBC
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3308/test?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456
##session store type
spring.session.store-type=none
#security login
#security.basic.enabled = false
jun.security.browser.loginPage=/newIndex.html
bash

最后看效果。测试一:先访问 Demo 登录页,结果如图9.11所示。

image 2024 04 01 00 03 44 331
Figure 4. 图9.11 测试结果一

测试二:访问服务,结果如图9.12所示。

image 2024 04 01 00 04 12 652
Figure 5. 图9.12 测试结果二

登录成功之后的处理

在登录成功之后,可以做一些处理,比如,进行自定义处理,具体的做法主要的思路有两部分,一是在配置文件中指定要处理的类,二是逻辑处理这个具体的类。因此,我们主要展示这两个部分的代码。配置类,指定登录之后的处理类,代码如下所示。

package com.cao.security.browser;
/**
  * @Description 覆盖security原有的配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
   //获取自定义的登录页面
   @Autowired
   private SecurityProperties securityProperties;
   //使用自己的登录成功后的处理类
   @Autowired
   private AuthenticationSuccessHandler browserAuthenticationSuccessHandler;
   @Override
   protected void configure(HttpSecurity http) throws Exception {
      //表单登录的一个安全认证环境
      http.formLogin()
          .loginPage("/authentication/require")
          .loginProcessingUrl("/authentication/form")
          .successHandler(browserAuthenticationSuccessHandler)
            // http.httpBasic()
          .and()
          .authorizeRequests() //请求授权
          .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll() //这个url不需要认证,包含自定义的登录页
          .anyRequest() //任何请求
          .authenticated() //都需要认证
          .and()
          .csrf().disable(); //去掉csrf的防护
   }
   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}
java

在上面的代码中,我们使用 successHandler 来指定登录成功之后的处理类,在这里指定一个 browserAuthenticationSuccessHandler 类进行处理。下面是登录成功之后的处理逻辑代码如下所示。

package com.cao.security.browser.authentication;
@Component(value="browserAuthenticationSucceswsHandler")
public class BrowserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
   private Logger logger=LoggerFactory.getLogger(getClass());
   @Autowired
   private ObjectMapper objectMapper;
   /**
   * @Description 登录成功会被调用
   */
   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
      //Authentication 封装了认证信息
      logger.info("登录成功");
      response.setContentType("application/json;charset=UTF-8");
      //将authentication转为json字符串response.getWriter().write(objectMapper.writeValueAsString(authenticat ion));
   }
}
java

在上面的代码中,需要让类实现 AuthenticationSuccessHandler,然后在方法中重写 onAuthenticationSuccess,具体的逻辑处理放在这个方法中。

登录失败之后的处理

对比登录成功之后的处理,登录失败之后同样可以进行一些处理,分两个部分。一个部分是配置项,另一个部分是配置文件具体指定的失败处理类。配置文件指定的失败处理类,代码如下所示。

package com.cao.security.browser;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
   //获取自定义的登录页面
   @Autowired
   private SecurityProperties securityProperties;
   //使用自己的登录成功后的处理类
   @Autowired
   private AuthenticationSuccessHandler browserAuthenticationSuccessHandler;
   //使用自己的登录失败后的处理类
   @Autowired
   private BrowserAuthenticationFailHandler browserAuthenticationFailHandler;
   @Override
   protected void configure(HttpSecurity http) throws Exception {
      //表单登录的一个安全认证环境
      http.formLogin()
          .loginPage("/authentication/require")
          .loginProcessingUrl("/authentication/form")
          .successHandler(browserAuthenticationSuccessHandler)
          .failureHandler(browserAuthenticationFailHandler)
            // http.httpBasic()
          .and()
          .authorizeRequests() //请求授权
          .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage()).permitAll()
           //这个url不需要认证,包含自定义的登录页
          .anyRequest() //任何请求
          .authenticated() //都需要认证
          .and()
          .csrf().disable(); //去掉csrf的防护
   }
   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}
java

在上面的程序中,使用 failureHandler 指定登录失败的处理类,这段代码直接写在登录成功代码的后面即可。然后,看看登录失败的处理代码,如下所示。

package com.cao.security.browser.authentication;
@Component(value="browserAuthenticationFailHandler")
public class BrowserAuthenticationFailHandler implements AuthenticationFailureHandler {
   private Logger logger=LoggerFactory.getLogger(getClass());
   @Autowired
   private ObjectMapper objectMapper;
   @Override
   public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
           AuthenticationException exception) throws IOException, ServletException {
      //这里不会有Authentication
      logger.info("登录失败");response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
      response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));
   }
}
java

在上面的代码中,需要让类实现 AuthenticationFailureHandler,然后在方法中重写 onAuthenticationFailure 方法,具体的逻辑处理放在这个方法中。