前言
个性化用户认证流程
- 自定义登录页面(之前是spring-security自带的)
- 登录成功处理(现在登录成功处理是跳转到接口对应的url上,但是我们想自定义一些业务逻辑:比如登录成功了给用户发一个积分等)
- 登录失败处理(密码失败了目前是显示一个错误信息,实际时候,我们可能还想记录日志,登录失败次数,超过一定次数不让其再次登录)
内容
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
为什么会出现死循环请求: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();//都需要认证
}
注意:我们表单里面的表单地址是:/authentication/form
注意:我们表单里面的表单地址是:/authentication/form
由于我们的过滤器:只会拦截login的POST
所以我们需要把/authentication/form 配置到UsernamePasswordAuthenticationFilter中去
登录页面为:
<!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>
重启服务后我们点击登录。
默认情况下: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();//跨站伪造的防护失效
}
然后重启服务,点击登录即可完成登录。
- 我们 发的是一个RestfulAPI请求:/user;但是这个接口需要认证返回的确实一个login.html,登录了login.html后才能访问此接口。Restful API放回的应该是状态码和json数据。
- 我们写了一个登录页面,但是我们的目的死提供一个可重用的安全模块.意味着:有多个项目使用这一个模块,不可能使用同一个标准登录,那么需要各个项目自定义自己 登录模块,没有的话就用默认的。
1.1第一个问题解决
我们 发的是一个RestfulAPI请求:/user;但是这个接口需要认证返回的确实一个login.html,登录了login.html后才能访问此接口。Restful API返回的应该是状态码和json数据。
我们之前是接到html请求或数据请求时候,是直接跳转到:login.html我们现在需要让其跳转到一个controller里面。
#此配置项存在的时候,系统走/demo-login.html,没有的话走标准;我们需要把配置项进行封装到类里面去
web:
security:
loginPage: /demo-login.html
属性配置类我们封装如下:
我们使用SecurityProperties类封装所有配置;这个类里面分为以下四种配置。
BrowserProperties:封装了浏览器安全相关配置项
ValidateCodeProperties:封装了验证码相关配置项
配置项内容我们写在:spring-security-core里面(因为app,web模块都会用到)
为了让整个配置类生效,我们还需要添加一个类:
声明好配置类之后,我们将其注入到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
响应:
然后前端处理跳转到登录页,让用户去登录。
访问页面:http://127.0.0.1:8088/index.html跳转到系统自己配置的页面上去:
去掉配置:
yxm:
security:
browser:
loginPage: /demo-login.html
访问.html时候会跳转到标准登录上去
2. 自定义登录成功处理
2.1 前言
- spring-security的默认处理在登录成功后会引发到之前登录请求上,比如:你去请求/user时候,会把你跳转到登录页上去,登录成功之后会跳回/user接口请求上。
- 现在spa情况下,登录可能不是表单提交方式来访问的。而是异步ajax方式来访问登录。前端请求时候只需要拿到一些登录请求的信息,你把他进行了跳转,显然是不合适的。那么就需要自定义登录成功之后的登陆行为。
- 在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测试:
返回了授权的基本信息:
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接口的子类有如下(每一种都代表不同的校验原理):
我们在配置类里面配置认证失败时候走我们自定义的失败处理器。
@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
输入后输出:
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.
@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
我们使用非正确的账号登录:
返回json
我们修改配置:
yxm:
security:
browser:
loginType: REDIRECT
然后我们在resources下添加index.html页面,我们在浏览器访问:
http://127.0.0.1:8088/index.html
输入正确信息:
返回:
我们在浏览器访问:
http://127.0.0.1:8088/index.html
输入错误信息,返回:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。