上一章:自定义用户认证逻辑(连数据库、校验逻辑、密码加密)
下一篇:https://blog.csdn.net/LawssssCat/article/details/105316362
个性化用户认证流程:
- 自定义登录页面
- 自定义登录成功处理
- 自定义登录失败处理
自定义登录页面
- 添加 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
登录成功
登录成功,转跳到指定的页面(页面没写)
登录失败
登录失败,返回指定的状态码(只需要捕获响应的错误,即可完成自定义)
done~~