上一篇文章中我们讲了Spring Security自定义用户认证逻辑。本篇文章将在上一篇文章的基础上做一些个性化用户的认证流程。所涉及的个性化用户认证流程有:
- 自定义登录页面
上一篇文章中我们用的登录表单依然是springsecurity的默认登录页面 - 自定义登录成功处理
有时候我们希望登录成功处理后不只是简单跳转到首页,我们还希望给用户颁发优惠券等等 - 自定义登录失败处理
当用户登录失败后,我们有时候也希望记录日志等信息,而不是像上一章节中直接弹出登录错误的提示。
1.自定义登录页面实战
1.1定义前端页面
siginIn.html<!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>
这里的登录页面只有用户名和密码加上一个登录按钮,然后页面名字为signIn.html
1.2添加配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/signIn.html")
.loginProcessingUrl("/authentication/form")
// http.httpBasic()
.and().authorizeRequests()// 表示下面是认证的配置
.antMatchers("/signIn.html").permitAll()
.anyRequest()// 任何请求
.authenticated()// 都需要身份认证
.and()
.csrf().disable();
}
为了能够使用我们的自定义页面,这里我们添加了loginPage("/signIn.html")。表单填写完成提交后,会被UsernamePasswordAuthenticationFilter拦截并验证用户名和密码,但是这个拦截器默认只拦截/login的请求路径,为了拦截我们提交的表单请求,所以要和signIn.html提交的/authentication/form路径一样。antMatchers("/signIn.html").permitAll()表示登录页面不需要认证,否则页面会不断重定向到自定义登录页面。csrf().disable()的添加是因为SpringSecurity默认会开启csrf安全机制,这里我们先关闭一下,否则我们会请求失败。
1.3总共需要添加的内容
1.4结果展示
OK,自定义登录页面的demo我们就成功搭建好了。
但是。。。
大家有没有发现。。。
2个问题。。。
第一个问题:我们如果发出去的是rest请求,返回的却是一个.html,而不是一个json应答。这有点违背restful的风格,因此我们要做一个改造。如果我们发起的是rest请求,就得到json应答,如果发起的是html请求,就跳转到登录页面。
第二个问题:我们的browser模块、app模块、core模块都是提供给外部服务的,引用我们模块的用户有时候希望定义自己的登录表单,而不是我们上面提供的默认的自定义表单。
我们的改造思路如下:
改造思路
2.改造
2.1在browser模块中自定义Controller
@RestController
public class BrowserSecurityController {
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 Exception {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是:" + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页面");
}
}
这个Controller的requestMapping是/authentication/require,我们之前配置的loginPage和antMathcers都必须有这个路径,这样用户没有登录的时候才会跳转到这个Controller里面来。
RequestCache用于获取引发跳转的请求路径,比如我们在浏览器中访问localhost:8080/index.html,此时SpringSecurity检测到我们没有登录则会跳转到这个Controller中,如果直接用request.getRequestURI看到的一定是/autheication/require,如下图所示:
为了知道用户是访问的是HTML还是rest接口,所以使用SpringSecurity提供的HttpSessionRequestCache。
这个Controller中如果发现请求的html,则会使用SpringSecurity的重定向策略DefaultRedirectStrategy,跳转到用户的配置页面redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());这里的securityProperties封装了所有的安全配置,包含浏览器的配置,OAuth等等配置,一会儿我们将在core模块中讲到他。如果用户请求的不是html,最后会返回一个json对象SimpleResponse给用户,且应答码是401,如果我们没有登录直接访问http://localhost:8080/user,结果如下图所示:
2.2core模块的改造
在2.1中我们看到SecurityProperties,这是因为我们所编写的模块是要供用户使用,为了提供可重用的模块,让使用我们core和browser、app模块的用户能够自定义自己的登录页面,所以我们将在core中编写一下这个SecurityProperties。如下图所示:
SecurityProperties
2.2.1 SecurityProperties
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
2.2.2 BrowserProperties
public class BrowserProperties {
private String loginPage="/signIn.html";
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
我们的BrowserProperties就只有一个loginPage,默认值就是用户没有自定义登录页面时候使用的browser提供的默认自定义页面。
2.2.3SecurityCoreConfig
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
@EnableConfigurationProperties注解的作用是:让使用 @ConfigurationProperties 注解的类生效。
2.3 demo自定义页面
2.3.1application.properties增加loginPage配置
imooc.security.browser.loginPage=/demo-signIn.html
2.3.2增加自定义页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>Demo登录页</h2>
</body>
</html>
2.4BrowserSecurityConfig配置改造
BrowserSecurityConfiglogingPage变成了controller的访问路径,antMatchers,除了controller的路径还有就是用户自定义的登录页面的路径。
2.5验证
自定义登录页面我们访问时跳转到了我们自定义的登录页面,如果我们把2.3.1application.properties的配置去掉,看到的就是如下的默认登录页面:
默认登录页面
3.自定义登录成功处理
3.1AuthenticationSuccessHandler
SpringSecurity提供了一个AuthenticationSuccessHandler接口,当用户认证成功后,会调用这个接口中唯一的方法onAuthenticationSuccess,所以我们只要重写这个方法就可以自定义我们登录成功的逻辑
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException;
}
其中的Authentication包含了登录成功后的用户的详细信息,这个也是我们重点要获取的对象
自定义Handler:
@Component("imoocAuthenticationSuccessHandler")
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
private RequestCache requestCache = new HttpSessionRequestCache();
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.authentication.
* AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
* HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginResponseType())) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
} else {
//调用父类的方法跳转
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
如果用户配置以JSON的方式响应,那么我们这里暂且返回authentication中的信息,如果是重定向的方式,我们就调用父类默认的跳转方法,这个父类SavedRequestAwareAuthenticationSuccessHandler实现了AuthenticationSuccessHandler接口
3.2BrowserSecurityConfig配置
BrowserSecurityConfig配置配置好登录成功要使用的handler。
3.3core模块BrowserProperties编写
3.4访问验证
我们来分析一下返回的json串,admin表示Userdetails里面返回的权限。principal里面是最最关键的内容,返回了用户的用户名和密码,不过密码默认被springSecurity设置为null了,authorities里面的信息就是Userdetails里面的信息。
4.自定义登录失败处理
4.1AuthenticationFailureHandler
用户认证失败时也有一个类似AuthenticationSuccessHandler的Handler,它就是AuthenticationFailureHandler
public interface AuthenticationFailureHandler {
/**
* Called when an authentication attempt fails.
* @param request the request during which the authentication attempt occurred.
* @param response the response.
* @param exception the exception which was thrown to reject the authentication
* request.
*/
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException;
}
自定义是失败的Handler
@Component("imoocAuthenctiationFailureHandler")
public class ImoocAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
/*
* (non-Javadoc)
*
* @see
* org.springframework.security.web.authentication.AuthenticationFailureHandler#
* onAuthenticationFailure(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.AuthenticationException)
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败");
if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginResponseType())) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
} else {
super.onAuthenticationFailure(request, response, exception);
}
}
}
AuthenticationException是用户登录失败后记录的具体异常信息
网友评论