上一章:自定义用户认证逻辑(连数据库、校验逻辑、密码加密)
下一篇:https://blog.csdn.net/LawssssCat/article/details/105316362

前面的代码下载:https://github.com/LawssssCat/v-security/tree/v2.1

个性化用户认证流程:

  • 自定义登录页面
  • 自定义登录成功处理
  • 自定义登录失败处理

自定义登录页面

  • 添加 loginPage 登录路径
  • 添加路径的许可 .antMatchers("/login.html").permitAll()

修改配置

package cn.vshop.security.browser;

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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/** * spring security 提供的 web 应用适配器 * * @author alan smith * @version 1.0 * @date 2020/4/3 12:15 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

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

        // 指定身份认证方式为表单
        http.formLogin()
                // 自定义登录页面
                .loginPage("/login.html")
                // 执行登录的URL
                .loginProcessingUrl("/authentication/form")
                .and()
                // 并且认证请求
                .authorizeRequests()
                // 设置,当访问到登录页面时,允许所有
                .antMatchers("/login.html").permitAll()
                // 全部请求,都需要认证
                .anyRequest().authenticated()
                .and()
                // 关闭 csrf 防护
                .csrf().disable();
    }

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

}

写一个登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h2>登录</h2>
<!--路径是我们自定义的,默认值为/login,在UsernamePasswordAuthenticationFilter中定义-->
<form method="post" action="/authentication/form">
    username: <input type="text" name="username"><br>
    pawssword: <input type="text" name="password"><br>
    <input type="submit" value="登录">
</form>

</body>
</html>

如果没有加 .antMatchers("/login.html").permitAll()
会出现如下错误,

这是因为,没有登录会重定向到登录页面,但没开放登录页面访问权限,就会不断重复这种重定向。

如果出现 Invalid CSRF Token ‘null’

<mark>那是因为 在默认情况下,SpringSecurity提供了 跨站请求伪造的一个防护,防护的方法通过CSRF Token来完成(后面讲攻击防护时候会专门讲)</mark>
也可以参考:cookies、攻击(xss、csrf)、防御(stp、sop)、开发(JSONP、WebSockets)
.
处理方法:暂时把 csrf 防护 关闭了


处理不同类型的请求

现在有两个问题

  • 如果自定义页面其实不是我们希望的,我们 REST 服务不希望返回 html,而是希望返回 包含状态码的 json 信息。

  • 不同项目(访问渠道)希望不同的登录页面,怎么做到?

那就需要一种处理不同类型的请求的服务

(业务流程如下图)

编写跳转Controller

package cn.vshop.security.browser;

import cn.vshop.security.browser.support.SimpleResponse;
import cn.vshop.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * @author alan smith * @version 1.0 * @date 2020/4/3 21:22 */
@Slf4j
@RestController
public class BrowserSecurityController {

    @Autowired
    private SecurityProperties securityProperties ;

    /** * 我们需要做判断,判断引发跳转的是否是html * 判断依据可以从 spring Security 提供的缓存中拿,因为 Spring Security 会把它的转跳请求放在 RequestCache 里面进行缓存 * 所以,我们现在就可以把缓存中的 request 拿出来进行比较 */
    private RequestCache requestCache = new HttpSessionRequestCache();


    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /** * 当需要身份人认证时,跳转到这里处理 * * @param request 请求 * @param response 响应 * @return 响应体 */
    @RequestMapping("/authentication/require")
    // 就是 401
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // 就是之前引发跳转,并缓存下载的那个请求
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest != null) {
            // 之歌字符串就是引发跳转的url
            // 比方说,我访问 /user 被拦截转跳达到 /login
            // 这里的 targetURL 就是 "http://localhost:8080/user"
            String targetURL = savedRequest.getRedirectUrl();
            log.info("引发跳转的请求是:{}", targetURL);
            // 判断引发转跳的url是否想访问一个页面
            if (StringUtils.endsWithIgnoreCase(targetURL, ".html")) {
                // 如果用户是想访问一个页面
                // 那么,就让他重定向到指定的url
                // 注意,这里的url不同项目可能不同,即不可以写死。
                // 我们决定在propertiest类里面配置
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }
        // 封装一个对象,专门返回信息/数据
        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
    }

}

编写属性类 xxxProperties

上面的 controller 涉及到 Properties 类。


而我们最终需要做成的属性类有5个,关系如下。
现在只做其中两个:SecurityProperties(最终被注入spring容器)和BrowserProperties(浏览器安全相关属性配置,会被作为前者的Field)

因为这个properties类跨域几个组件,因此,<mark>我们把它放在 core 模块中</mark>

SecurityProperties

package cn.vshop.security.core.properties;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/** * @author alan smith * @version 1.0 * @date 2020/4/3 22:03 */
@Getter
@Setter
@ConfigurationProperties(prefix = "v.security")
public class SecurityProperties {

    // 这里读取的是 v.security.browser 配置项
    private BrowserProperties browser = new BrowserProperties();

}

BrowserProperties

package cn.vshop.security.core.properties;


/** * Browser 项目(浏览器安全)相关的配置项 * * @author alan smith * @version 1.0 * @date 2020/4/3 22:01 */
public class BrowserProperties {

    // 登录页
    private String loginPage = "/login.html";

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }
}

<mark>对应的,我们在 application.yml 上写属性值</mark>

v:
  security:
    browser:
      # 登录页面
      loginPage: /login2.html

代表的是,访问失败,转跳到的登录页

SecurityConfig

最后,写一个配置类,让属性生效

package cn.vshop.security.core;

import cn.vshop.security.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/** * 让我们配置的 properties 生效 * * @author alan smith * @version 1.0 * @date 2020/4/3 22:08 */
@Configuration
// 指定想要使其生效的配置器
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}

修改 browser 的配置类

在配置里,把登录页面的 URL 改为我们上面写好的 Controller 映射

测试

访问 : http://localhost:8080/随便一个请求


访问 : http://localhost:8080/随便一个请求.html

自定义登录成功处理

spring security 默认在登录成功后,跳转到先前的访问页面。
但是,<mark>如今前后端分离,更多的是用ajax异步请求登录,转跳明显是不再合适了</mark>

下面进行自定义的登录成功处理

实现接口 AuthenticationSuccessHandler

实际上,我们只需要实现接口 AuthenticationSuccessHandler ,即可自定义成功

package cn.vshop.security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * @author alan smith * @version 1.0 * @date 2020/4/4 11:09 */
@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    // 对象到json串的转换器,spring启动时自动注册
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            // 封装认证信息,包括认证请求中的信息(如session、ip)、UserDetails
            Authentication authentication)
            throws IOException, ServletException {
        log.info("登录成功!");

        // 响应类型信息
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        // 响应体信息
        // 把Authentication实例转为一个json字符串
        response.getWriter().write(objectMapper.writeValueAsString(authentication));

    }

}

修改 BrowserSecurityConfig
修改配置,让springsecurity知道,要用我们自定义的处理器,而不是默认的。

测试行为

访问 : http://localhost:8080/随便一个请求.html

转跳到登录页面

输入密码 123456

(下图)自定义<mark>成功</mark>处理成功


<mark>至于真正要返回什么 json 数据,我们处理业务时候再处理</mark>

自定义登录失败处理

失败就跟成功一样了

实现接口:AuthenticationFailureHandler

package cn.vshop.security.browser.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * @author alan smith * @version 1.0 * @date 2020/4/4 11:33 */
@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request,
            HttpServletResponse response,
            // 因为没有登录成功,因此没有用信息
            // 取而代之的,是认证过程中发生的异常信息
            AuthenticationException exception
    ) throws IOException, ServletException {

        log.info("登录失败");
        // 因为登录失败,不能返回默认的200信息,而是500(看需求)
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        // 响应头
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        // 相应的,我们这里返回异常的json信息
        response.getWriter().write(objectMapper.writeValueAsString(exception));

    }
}

在配置中添加

测试行为

访问 : http://localhost:8080/随便一个请求.html

转跳到登录页面

输入密码 11111(错误)

(下图)自定义<mark>错误</mark>处理成功

抽离可配置项

让用户自己决定登录成功或失败后的行为,是返回页面还是json

编写行为枚举类

枚举类指定登录成功或失败后的行为,返回 json?还是重定向到指定页面?

package cn.vshop.security.core.properties;

/** * 登录成功后的行为 * * @author alan smith * @version 1.0 * @date 2020/4/4 18:05 */
public enum LoginType {

    REDIRECT,

    JSON

}

在属性类中添加相应属性
添加 LoginType 属性

package cn.vshop.security.core.properties;


import lombok.Getter;
import lombok.Setter;

/** * Browser 项目(浏览器安全)相关的配置项 * * @author alan smith * @version 1.0 * @date 2020/4/3 22:01 */
@Getter
@Setter
public class BrowserProperties {

    /** * 自定义登录成功后的行为 */
    private LoginType loginType = LoginType.JSON;

    /** * 登录页 */
    private String loginPage = "/login.html";

}

修改登录成功处理器

修改为继承 SavedRequestAwareAuthenticationSuccessHandler (这是 spring Security 默认的处理器,即默认重定向)
我们只需要注入定义的配置属性,进行判断,判断配置的是使用 json 还是 重定向
如果是json的话,就用我们写的方法。

package cn.vshop.security.browser.authentication;

import cn.vshop.security.core.properties.LoginType;
import cn.vshop.security.core.properties.SecurityProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * @author alan smith * @version 1.0 * @date 2020/4/4 11:09 */
@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler
        // 继承 Spring Security 默认的处理器,在他上面添加 json返回功能
        extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private SecurityProperties securityProperties;

    // 对象到json串的转换器,spring启动时自动注册
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            // 封装认证信息,包括认证请求中的信息(如session、ip)、UserDetails
            Authentication authentication)
            throws IOException, ServletException {
        log.info("登录成功!");

        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            // 判断如果我们定义的登录类型是 json,那么就用我们自己的方式 返回json
            // 响应类型信息
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            // 响应体信息
            // 把Authentication实例转为一个json字符串
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        } else {
            // 否则,就用父类的跳转
            super.onAuthenticationSuccess(request, response, authentication);
        }


    }

}

修改登录失败处理器

package cn.vshop.security.browser.authentication;

import cn.vshop.security.core.properties.LoginType;
import cn.vshop.security.core.properties.SecurityProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * @author alan smith * @version 1.0 * @date 2020/4/4 11:33 */
@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler
        // 继承SpringSecurity默认登录失败后的处理器,在其上做扩展
        extends ExceptionMappingAuthenticationFailureHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request,
            HttpServletResponse response,
            // 因为没有登录成功,因此没有用信息
            // 取而代之的,是认证过程中发生的异常信息
            AuthenticationException exception
    ) throws IOException, ServletException {

        log.info("登录失败");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            // 因为登录失败,不能返回默认的200信息,而是500(看需求)
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            // 响应头
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            // 相应的,我们这里返回异常的json信息
            response.getWriter().write(objectMapper.writeValueAsString(exception));

        } else {
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}

修改配置为JSON

这时我们配置 loginType 为 JSON 或者不配置,那么不登录,访问:http://localhost:8080/user 返回的就是 json 模式

v:
  security:
    browser:
      loginType: JSON
      # 或者不配置

登录失败

登录地址:http://localhost:8080/login2.html

返回失败的json(500状态码)

登录成功

json(200状态码)

修改配置为 REDIRECT

v:
  security:
    browser:
      loginType: REDIRECT

登录成功

登录成功,转跳到指定的页面(页面没写)

登录失败
登录失败,返回指定的状态码(只需要捕获响应的错误,即可完成自定义)

前面的代码下载:https://github.com/LawssssCat/v-security/tree/v2.2

done~~