赞
踩
上一章:自定义用户认证逻辑(连数据库、校验逻辑、密码加密)
下一篇:https://blog.csdn.net/LawssssCat/article/details/105316362
个性化用户认证流程:
修改配置
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’
那是因为 在默认情况下,SpringSecurity提供了 跨站请求伪造的一个防护,防护的方法通过CSRF Token来完成(后面讲攻击防护时候会专门讲)
也可以参考: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类跨域几个组件,因此,我们把它放在 core 模块中
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; } }
对应的,我们在 application.yml 上写属性值
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 默认在登录成功后,跳转到先前的访问页面。
但是,如今前后端分离,更多的是用ajax异步请求登录,转跳明显是不再合适了
下面进行自定义的登录成功处理
实现接口 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
(下图)自定义成功处理成功
至于真正要返回什么 json 数据,我们处理业务时候再处理
失败就跟成功一样了
实现接口: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(错误)
(下图)自定义错误处理成功
让用户自己决定登录成功或失败后的行为,是返回页面还是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
登录成功
登录成功,转跳到指定的页面(页面没写)
登录失败
登录失败,返回指定的状态码(只需要捕获响应的错误,即可完成自定义)
done~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。