前言

个性化用户认证流程

  1. 自定义登录页面(之前是spring-security自带的)
  2. 登录成功处理(现在登录成功处理是跳转到接口对应的url上,但是我们想自定义一些业务逻辑:比如登录成功了给用户发一个积分等)
  3. 登录失败处理(密码失败了目前是显示一个错误信息,实际时候,我们可能还想记录日志,登录失败次数,超过一定次数不让其再次登录)

内容

1. 自定义登录页面

在WebSecurityConfig里面:configure

@Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 定义了任何请求都需要表单认证
         */
       http.formLogin()//表单登录---指定了身份认证方式
           .loginPage("/login.html")
       // http.httpBasic()//http的basic登录
          .and()
          .authorizeRequests()//对请求进行授权
          .anyRequest()//任何请求
          .authenticated();//都需要认证
    }

我们重启服务并且在浏览器输入:
http://127.0.0.1:8088/user
image.png

为什么会出现死循环请求:login.html呢?
原因是我们配置了登录页:.loginPage("/login.html")
但是所有的请求都需要身份认证:
.authorizeRequests()//对请求进行授权
.anyRequest()//任何请求
.authenticated();//都需要认证

意思就是当我们访问:/user时候,因为需要身份认证,所以跳转到了:/login.html

但是/login.html也是需要认证的,又跳到自身/login.html 因此造成了死循环,原因就是没有配置授权。所以我们单独授权下:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 定义了任何请求都需要表单认证
         */
       http.formLogin()//表单登录---指定了身份认证方式
           .loginPage("/login.html")
       // http.httpBasic()//http的basic登录
          .and()
          .authorizeRequests()//对请求进行授权
          .antMatchers("/login.html").permitAll()//对匹配login.html的请求允许访问
          .anyRequest()//任何请求
          .authenticated();//都需要认证
}

image.png

注意:我们表单里面的表单地址是:/authentication/form

注意:我们表单里面的表单地址是:/authentication/form

由于我们的过滤器:只会拦截login的POST
image.png

所以我们需要把/authentication/form 配置到UsernamePasswordAuthenticationFilter中去
image.png

登录页面为:

<!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>

重启服务后我们点击登录。
image.png
默认情况下:spring security提供了跨站伪造的防护,用CSRF Token表示 为了临时让其 通过我们在配置里面让其失效:

/**
     * 定义web安全配置类:覆盖config方法
     * 1.参数为HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 定义了任何请求都需要表单认证
         */
       http.formLogin()//表单登录---指定了身份认证方式
           .loginPage("/login.html")
           .loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
       // http.httpBasic()//http的basic登录
          .and()
          .authorizeRequests()//对请求进行授权
          .antMatchers("/login.html").permitAll()//对匹配login.html的请求允许访问
          .anyRequest()//任何请求
          .authenticated()
           .and()
           .csrf().disable();//跨站伪造的防护失效
    }

然后重启服务,点击登录即可完成登录。

  1. 我们 发的是一个RestfulAPI请求:/user;但是这个接口需要认证返回的确实一个login.html,登录了login.html后才能访问此接口。Restful API放回的应该是状态码和json数据。
  2. 我们写了一个登录页面,但是我们的目的死提供一个可重用的安全模块.意味着:有多个项目使用这一个模块,不可能使用同一个标准登录,那么需要各个项目自定义自己 登录模块,没有的话就用默认的。
1.1第一个问题解决

我们 发的是一个RestfulAPI请求:/user;但是这个接口需要认证返回的确实一个login.html,登录了login.html后才能访问此接口。Restful API返回的应该是状态码和json数据。

我们之前是接到html请求或数据请求时候,是直接跳转到:login.html我们现在需要让其跳转到一个controller里面。

image.png

#此配置项存在的时候,系统走/demo-login.html,没有的话走标准;我们需要把配置项进行封装到类里面去
web:
  security:
     loginPage: /demo-login.html

属性配置类我们封装如下:

我们使用SecurityProperties类封装所有配置;这个类里面分为以下四种配置。

BrowserProperties:封装了浏览器安全相关配置项
ValidateCodeProperties:封装了验证码相关配置项

配置项内容我们写在:spring-security-core里面(因为app,web模块都会用到)

image.png

为了让整个配置类生效,我们还需要添加一个类:

声明好配置类之后,我们将其注入到spring-security-web里面:WebSecurityController,然后我们将跳转页跳转到对应的配置页面下

@RestController
public class WebSecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
      //从sesson缓存中拿到SavedRequest
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        //如果是不为空且为html请求登录页面,我们重定向到登录页,否则以封装string的SimpleResponse返回接口401没有授权
        if(savedRequest != null){
            String redirectUrl = savedRequest.getRedirectUrl();
            logger.info("引发跳转的请求是:" + redirectUrl);
            if(StringUtils.endsWithIgnoreCase(redirectUrl,".html")){
                //此处第3个参数就是我们需要 根据多个终端定制化返回。
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }
        //否则返回401错误
        return new SimpleResponse("访问的服务需要身份认证,请跳转到登录页");
    }
}

为了让其默认情况下跳转到标准登录页,我们在BrowserProperties配置。

public class BrowserProperties {
    private String loginPage = "/login.html";

    public String getLoginPage() {
        return loginPage;
    }

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

我们在授权配置页面添加响应的页面跳转权限:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;
    @Bean
    public PasswordEncoder  passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 定义web安全配置类:覆盖config方法
     * 1.参数为HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        /**
         * 定义了任何请求都需要表单认证
         */
       http.formLogin()//表单登录---指定了身份认证方式
          // .loginPage("/login.html")
               .loginPage("/authentication/require")
           .loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
       // http.httpBasic()//http的basic登录
          .and()
          .authorizeRequests()//对请求进行授权
          .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage()).permitAll()//对匹配login.html的请求允许访问
          .anyRequest()//任何请求
          .authenticated()
           .and()
           .csrf().disable();//都需要认证
    }
}

测试:
访问接口:
http://127.0.0.1:8088/user
响应:
image.png

然后前端处理跳转到登录页,让用户去登录。
访问页面:http://127.0.0.1:8088/index.html跳转到系统自己配置的页面上去:
image.png

去掉配置:

yxm:
  security:
     browser:
      loginPage: /demo-login.html

访问.html时候会跳转到标准登录上去

2. 自定义登录成功处理

2.1 前言

  1. spring-security的默认处理在登录成功后会引发到之前登录请求上,比如:你去请求/user时候,会把你跳转到登录页上去,登录成功之后会跳回/user接口请求上。
  2. 现在spa情况下,登录可能不是表单提交方式来访问的。而是异步ajax方式来访问登录。前端请求时候只需要拿到一些登录请求的信息,你把他进行了跳转,显然是不合适的。那么就需要自定义登录成功之后的登陆行为。
  3. 在spring-security下实现登录成功后处理只需要实现AuthenticationSuccessHandler接口。

2.2代码实现

我们在spring-security-web工程里面新建
认证信息都封装到参数:authentication里面,登录成功之后我们是需要把 authentication里面的信息转换成json传递给前端去。
Spring启动时候会自动注入一个ObjectMapper;ObjectMapper(com.fasterxml.jackson.databind.ObjectMapper)可以将authentication转换成json字符串。

@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        /**
         * 将Authentication转换成json返回给前端
         * 参数:authentication 使用不同登录方式,其值是不一样的,这是一个接口在实际运转中,他会传不同的实现对象过来  
         */
        logger.info("登录成功");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

我们还需要一个配置,告诉Spring security我们登录成功之后用我们自己写的登录成功处理器去做处理。而不用spring默认的处理器。在formLogin()下面加配置。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Bean
    public PasswordEncoder  passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 定义web安全配置类:覆盖config方法
     * 1.参数为HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 定义了任何请求都需要表单认证
         */
       http.formLogin()//表单登录---指定了身份认证方式
          // .loginPage("/login.html")
           .loginPage("/authentication/require")
           .loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
           .successHandler(myAuthenticationSuccessHandler)//表单登录成功弄之后用自带的处理器
       // http.httpBasic()//http的basic登录
          .and()
          .authorizeRequests()//对请求进行授权
          .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage()).permitAll()//对匹配login.html的请求允许访问
          .anyRequest()//任何请求
          .authenticated()
           .and()
           .csrf().disable();//都需要认证
    }
}

web测试:
image.png

返回了授权的基本信息:
image.png

3. 自定义登录失败处理(将失败信息返回给前端)

失败处理和成功处理一致:如果客户端是一个异步请求的话,我们也应该返回授权信息,而不是跳转到登录页面上去。

实现AuthenticationFailureHandler接口,声明为Spring组件。

@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler 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 {
        /**
         * 1.第三个参数不是Authentication了,因为是登录失败抛异常了,所以是:AuthenticationException
         * 2.因为是登录失败,所以我们返回的时候状态码不再是200,而是500
         */
        logger.info("登录失败");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));
    }
}

AuthenticationException接口的子类有如下(每一种都代表不同的校验原理):
image.png

我们在配置类里面配置认证失败时候走我们自定义的失败处理器。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Bean
    public PasswordEncoder  passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 定义web安全配置类:覆盖config方法
     * 1.参数为HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 定义了任何请求都需要表单认证
         */
       http.formLogin()//表单登录---指定了身份认证方式
          // .loginPage("/login.html")
           .loginPage("/authentication/require")
           .loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
           .successHandler(myAuthenticationSuccessHandler)//表单登录成功之后用自带的处理器
           .failureHandler(myAuthenticationFailureHandler)//表单登录失败之后用自带的处理器
       // http.httpBasic()//http的basic登录
          .and()
          .authorizeRequests()//对请求进行授权
          .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage()).permitAll()//对匹配login.html的请求允许访问
          .anyRequest()//任何请求
          .authenticated()
           .and()
           .csrf().disable();//都需要认证
    }
}

web端测试,我们在标准登录页面输入错误的用户名/密码:Jack/1234567 image.png

输入后输出:
image.png

4. 封装

我们如果想实现一个通用的安全框架,但是我们现在把登录成功/失败写成现在这个样子;写死(返回json形式)也是不合适的。因为有些应用就是jsp和前端模板引擎页面这种登录同步提交的方式。所以需要我们对代码进行改造,让其同时满足同步登录(跳转)和异步ajax请求登录(返回json)。让用户通过自己的配置去决定 跳转还是返回json。

我们在BrowserProperties添加一个属性(登录类型):

public class BrowserProperties {
    private String loginPage = "/login.html";

    private LoginType loginType = LoginType.JSON;

    public String getLoginPage() {
        return loginPage;
    }

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

    public LoginType getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginType loginType) {
        this.loginType = loginType;
    }
}

LoginType枚举:

public enum LoginType {
    /**
     * 跳转
     */
    REDIRECT,
    /**
     * 返回json
     */
    JSON
}

我们修改登录成功处理器:让其集成Spring提供的SavedRequestAwareAuthenticationSuccessHandler:
其继承了(extends) SimpleUrlAuthenticationSuccessHandler;SimpleUrlAuthenticationSuccessHandler实现了接口:AuthenticationSuccessHandler.

image.png

@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        /**
         * 将Authentication转换成json返回给前端
         * 参数:authentication 使用不同登录方式,其值是不一样的,这是一个接口在实际运转中,他会传不同的实现对象过来
         */
        logger.info("登录成功");
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){//JSON异步登录
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else {
            //非json就是使用父类处理器---父类处理器就是跳转
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }
}

失败处理器类似:

@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler{
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        /**
         * 1.第三个参数不是Authentication了,因为是登录失败抛异常了,所以是:AuthenticationException
         * 2.因为是登录失败,所以我们返回的时候状态码不再是200,而是500
         */
        logger.info("登录失败");

        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){//JSON
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception));
        }else {
            super.onAuthenticationFailure(request,response,exception);
        }
    }
}

系统默认LoginType=JSON
测试:
我们使用正确的账号的账号登录:
返回json
image.png

我们使用非正确的账号登录:
返回json

image.png

我们修改配置:

yxm:
  security:
     browser:
       loginType: REDIRECT

然后我们在resources下添加index.html页面,我们在浏览器访问:
http://127.0.0.1:8088/index.html
输入正确信息:
image.png
返回:
image.png

我们在浏览器访问:
http://127.0.0.1:8088/index.html
输入错误信息,返回:
image.png


startshineye
91 声望26 粉丝

我在规定的时间内,做到了我计划的事情;我自己也变得自信了,对于外界的人跟困难也更加从容了,我已经很强大了。可是如果我在规定时间内,我只有3分钟热度,哎,我不行,我就放弃了,那么这个就是我自己的问题,因为你自己...


引用和评论

0 条评论