个性化用户认证流程
一, 自定义登录页面
加页面: 定义该页面 hcx-signIn.html 为登录页面:
配授权
- @Configuration
- public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
- @Bean // 加密
- public PasswordEncoder passwordEncoder() {
- // 如果系统本身有了其他的加密方式, 此处就应该返回自己写的 passwordencoder, 再实现 encoder 和 matches 方法
- return new BCryptPasswordEncoder();
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // 使用表单登录: 指定了身份认证的方式
- http.formLogin()
- .loginPage("/hcx-signIn.html")// 自定义登录页面
- //http.httpBasic() // 使用回之前的认证方式
- .and()
- .authorizeRequests()// 表示以下都是授权的配置
- .antMatchers("/hcx-signIn.html").permitAll()// 访问该 url 不需要身份认证
- .anyRequest()// 任何请求
- .authenticated();// 都需要身份认证
- }
- }
- hcx-signIn.html:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title > 登录 </title>
- </head>
- <body>
- <h2 > 标准登录页面 </h2>
- </body>
- </html>
登录页面目录. png
注意, 如果忘记配授权的话就会进入死循环:
死循环. png
运行后访问 http://localhost:8060/user :
访问结果页面. png
过滤器默认处理的登录请求是 / login post 形式
如果使用了新的请求路径, 还需要配置, 让 SpringSecurity 知道
hcx-signIn.html:
- <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>
- MyUserDetailsService:
- @Component
- public class MyUserDetailsService implements UserDetailsService{
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Autowired
- private PasswordEncoder passwordEncoder;
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- logger.info("登录用户名:"+username);
- // 根据用户名查找用户信息
- // 根据查找到的用户信息判断用户是否被冻结
- // 为了把用户是否冻结的信息告诉 SpringSecurity,new User 的构造方法使用包含四个布尔返回值的参数的方法
- // 此处没有读取数据库, 直接用静态数据 密码: 123456 静态权限: admin 这些在实际开发中需要从数据库中获取
- //passwordEncoder.encode("123456"), 在实际应用中, 此步骤应该在注册的时候就做好了, 此处就直接在数据库拿出加密好的数据
- String password = passwordEncoder.encode("123456");
- logger.info("数据库密码是:"+password);
- return new User(username,password,
- true,true,true,false,
- AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
- }
- /**
- * 此处, 当前的方法 loadUserByUsername 返回的是 UserDetails 接口的实例, 使用了 Spring 默认的 User 类
- * 实际的应用中, 并不一定更要使用该类, 只要是 UserDetails 这个接口的实现就可以
- * 可以使用对应的 DAO 接口实现 UserDetails 接口
- */
- }
- BrowserSecurityConfig:
- @Configuration
- public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
- @Bean // 加密
- public PasswordEncoder passwordEncoder() {
- // 如果系统本身有了其他的加密方式, 此处就应该返回自己写的 passwordencoder, 再实现 encoder 和 matches 方法
- return new BCryptPasswordEncoder();
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // 使用表单登录: 指定了身份认证的方式
- http.formLogin()
- .loginPage("/hcx-signIn.html")// 自定义登录页面
- .loginProcessingUrl("/authentication/form") // 配置让 Spring 知道让 UsernamePasswordAuthenticationFilter 过滤器去处理 / authentication/form 路径
- //http.httpBasic() // 使用回之前的认证方式
- .and()
- .authorizeRequests()// 表示以下都是授权的配置
- .antMatchers("/hcx-signIn.html").permitAll()// 当访问 hcx-signIn.html 时, 不需要身份认证
- .anyRequest()// 任何请求
- .authenticated()// 都需要身份认证
- .and()
- .csrf().disable();
- }
- }
表单登录页面. png
改进: 处理不同类型的请求
把上面直接是跳转到一个页面, 换成一个 Controller, 让 Controller 判断是否是一个 HTML 请求引发的跳转, 如果是就返回登录页面如果不是就返回 401 状态码和错误信息:
处理不同类型的请求. png
BrowserSecurityController: 在该类中处理需要身份认证的请求:
- @RestController
- public class BrowserSecurityController {
- private Logger logger = LoggerFactory.getLogger(getClass());
- // 判断引发跳转的是否是 html
- // 用 RequestCache 拿到引发跳转的请求
- private RequestCache requestCache = new HttpSessionRequestCache();
- private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
- @Autowired
- private SecurityProperties securityProperties;
- /**
- * 当需要身份认证时, 跳转到这里
- * @param request
- * @param response
- * @return
- * @throws IOException
- */
- @RequestMapping("/authentication/require")
- @ResponseStatus(code=HttpStatus.UNAUTHORIZED) // 返回状态码
- public SimpleResponse requireAuthentication(HttpServletRequest request,HttpServletResponse response) throws IOException{
- // 拿到引发跳转的请求
- SavedRequest savedRequest = requestCache.getRequest(request, response);
- if(savedRequest!=null) {
- // 引发跳转请求的 url
- String targetUrl = savedRequest.getRedirectUrl();
- logger.info("引发跳转的请求时:"+targetUrl);
- if(StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {// 判断引发跳转的请求是否是以. html 结尾
- // 跳转到登录页
- /**
- * request
- * response
- * url: 要跳转的 url 此处不可能固定的跳转到某一个页面, 配置可以使用标准登录页还是使用自己写的登录页
- */
- redirectStrategy.sendRedirect(request, response,securityProperties.getBrowser().getLoginPage());//url:// 跳转到用户配置的 login 的配置
- }
- }
- // 如果不是一个 html 请求, 返回 401 状态码和错误信息
- return new SimpleResponse("访问的服务需要身份认证, 请引导用户到登录页");
- }
- }
使用户可以自己去配登录页面:
在 application.properties 中配置:
- hcx.security.browser.loginPage = /demo-signIn.html
- demo-signIn.html:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title > 登录 </title>
- </head>
- <body>
- <h2>Demo 登录页 </h2>
- </body>
- </html>
当做了该配置之后, 就会跳转到 demo-signIn.html 该页面; 如果没有该配置, 则跳转到原本配置的标准登录页
实现配置跳转到不同登录页. png
系统配置封装:
系统配置封装. png
- SecurityProperties:
- package com.hcx.security.core.properties;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- /**
- * @author HCX
- *
- */
- @ConfigurationProperties(prefix="hcx.security")// 该类会读取配置文件中所有以 hcx.security 开头的配置项
- public class SecurityProperties {
- private BrowserProperties browser = new BrowserProperties();
- public BrowserProperties getBrowser() {
- return browser;
- }
- public void setBrowser(BrowserProperties browser) {
- this.browser = browser;
- }
- }
- BrowserProperties:
- /**
- * @author HCX
- *
- */
- public class BrowserProperties {
/**
* 如果用户配置了就使用用户配置的;
* 如果没有配, 则使用 / hcx-signIn.html
*/
- private String loginPage = "/hcx-signIn.html";// 指定默认跳转
- public String getLoginPage() {
- return loginPage;
- }
- public void setLoginPage(String loginPage) {
- this.loginPage = loginPage;
- }
- }
- BrowserSecurityConfig:
- @Configuration
- public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
- @Autowired
- private SecurityProperties securityProperties;
- @Bean // 加密
- public PasswordEncoder passwordEncoder() {
- // 如果系统本身有了其他的加密方式, 此处就应该返回自己写的 passwordencoder, 再实现 encoder 和 matches 方法
- return new BCryptPasswordEncoder();
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // 使用表单登录: 指定了身份认证的方式
- http.formLogin()
- .loginPage("/authentication/require")// 自定义登录页面
- .loginProcessingUrl("/authentication/form") // 配置让 Spring 知道让 UsernamePasswordAuthenticationFilter 过滤器去处理 / authentication/form 路径
- //http.httpBasic() // 使用回之前的认证方式
- .and()
- .authorizeRequests()// 表示以下都是授权的配置
- .antMatchers("/authentication/require",
- securityProperties.getBrowser().getLoginPage()).permitAll()
- .anyRequest()// 任何请求
- .authenticated()// 都需要身份认证
- .and()
- .csrf().disable();
- }
- }
要使配置生效, 还需要一个配置类:
- @Configuration // 声明其为一个配置类
- @EnableConfigurationProperties(SecurityProperties.class)// 作用: 让 SecurityProperties 配置读取器生效
- public class SecurityCoreConfig {
- }
使用注解返回状态码:@ResponseStatus(code=HttpStatus.UNAUTHORIZED)
返回错误信息: 服务应该返回 json
包装, 把字符串包装成对象返回
- public class SimpleResponse {
- private Object content;
- public Object getContent() {
- return content;
- }
- public SimpleResponse(Object content) {
- super();
- this.content = content;
- }
- public void setContent(Object content) {
- this.content = content;
- }
- }
运行访问: localhost:8060/user:
运行结果 1.png
访问: localhost:8060/index.html 则跳转到系统配置的登录页, 如果没有配置 hcx.security.browser.loginPage = /demo-signIn.html, 则跳转到标准登录页
二, 自定义登录成功处理
场景: 默认情况下, SpringSecurity 的登录成功的处理会首先跳到之前引发登录的请求上, 比如访问 / user, 需要身份认证, 就会跳转到登录页, 登录成功了, 又会跳回 user 请求上. 但是在现在前端 spa 比较流行的情况下, 登录可能不是一个表单提交的同步方式, 而是由异步的 ajax 请求访问登录. 此时, 前端想要拿到的是用户相关的 json 格式的信息, 此时如果登录成功了进行跳转, 此种行为肯定是不合适的.
实现 AuthenticationSuccessHandler 接口即可.
自定义成功处理器 HCXAuthenticationSuccessHandler:
- package com.hcx.security.browser.authentication;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
- import org.springframework.stereotype.Component;
- @Component("hcxAuthenticationSuccessHandler")
- public class HCXAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Autowired
- private ObjectMapper objectMapper;
- // 登录成功后会被调用
/**
* Authentication: 封装认证信息: 包括发起的认证请求的信息, 比如 IP session 和用户信息等
*/
- @Override
- public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
- logger.info("登录成功");
- response.setContentType("application/json;charset=UTF-8");
- response.getWriter().write(objectMapper.writeValueAsString(authentication));
- }
- }
配置: 让 SpringSecurity 知道在登录成功以后用自己定义的登录成功处理器来处理, 而不是用 Spring 默认的处理器, 修改配置类
注入自定义的成功处理器. png
- BrowserSecurityConfig:
- package com.hcx.security.browser;
- import org.springframework.beans.factory.annotation.Autowired;
- 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;
- import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
- import com.hcx.security.core.properties.SecurityProperties;
- @Configuration
- public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
- @Autowired
- private SecurityProperties securityProperties;
- // 注入自己写的登录成功处理器
- @Autowired
- private AuthenticationSuccessHandler hcxAuthenticationSuccessHandler;
- @Bean // 加密
- public PasswordEncoder passwordEncoder() {
- // 如果系统本身有了其他的加密方式, 此处就应该返回自己写的 passwordencoder, 再实现 encoder 和 matches 方法
- return new BCryptPasswordEncoder();
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // 使用表单登录: 指定了身份认证的方式
- http.formLogin()
- .loginPage("/authentication/require")// 自定义登录页面
- .loginProcessingUrl("/authentication/form") // 配置让 Spring 知道让 UsernamePasswordAuthenticationFilter 过滤器去处理 / authentication/form 路径
- //http.httpBasic() // 使用回之前的认证方式
- .successHandler(hcxAuthenticationSuccessHandler)
- .and()
- .authorizeRequests()// 表示以下都是授权的配置
- .antMatchers("/authentication/require",
- securityProperties.getBrowser().getLoginPage()).permitAll()
- .anyRequest()// 任何请求
- .authenticated()// 都需要身份认证
- .and()
- .csrf().disable();
- }
- }
返回的 json.png
三, 自定义登录失败处理
失败处理器 HCXAuthencationFailHandler:
- package com.hcx.security.browser.authentication;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.HttpStatus;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.web.authentication.AuthenticationFailureHandler;
- import org.springframework.stereotype.Component;
- import com.fasterxml.jackson.databind.ObjectMapper;
- @Component("hcxAuthenticationFailHandler")
- public class HCXAuthencationFailHandler implements AuthenticationFailureHandler {
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Autowired
- private ObjectMapper objectMapper;
- /**
- * AuthenticationException: 认证过程中发生错误产生异常的信息
- */
- @Override
- public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
- AuthenticationException exception) throws IOException, ServletException {
- logger.info("登录失败");
- response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
- response.setContentType("application/json;charset=UTF-8");
- response.getWriter().write(objectMapper.writeValueAsString(exception));
- }
- }
使失败处理器生效的配置:
注入自定义的失败处理器. png
- BrowserSecurityConfig:
- package com.hcx.security.browser;
- import org.springframework.beans.factory.annotation.Autowired;
- 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;
- import org.springframework.security.web.authentication.AuthenticationFailureHandler;
- import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
- import com.hcx.security.core.properties.SecurityProperties;
- @Configuration
- public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
- @Autowired
- private SecurityProperties securityProperties;
- // 注入自己写的登录成功处理器
- @Autowired
- private AuthenticationSuccessHandler hcxAuthenticationSuccessHandler;
- // 注入自己写的登录失败处理器
- @Autowired
- private AuthenticationFailureHandler hcxAuthencationFailHandler;
- @Bean // 加密
- public PasswordEncoder passwordEncoder() {
- // 如果系统本身有了其他的加密方式, 此处就应该返回自己写的 passwordencoder, 再实现 encoder 和 matches 方法
- return new BCryptPasswordEncoder();
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // 使用表单登录: 指定了身份认证的方式
- http.formLogin()
- .loginPage("/authentication/require")// 自定义登录页面
- .loginProcessingUrl("/authentication/form") // 配置让 Spring 知道让 UsernamePasswordAuthenticationFilter 过滤器去处理 / authentication/form 路径
- //http.httpBasic() // 使用回之前的认证方式
- .successHandler(hcxAuthenticationSuccessHandler)
- .failureHandler(hcxAuthencationFailHandler)
- .and()
- .authorizeRequests()// 表示以下都是授权的配置
- .antMatchers("/authentication/require",
- securityProperties.getBrowser().getLoginPage()).permitAll()
- .anyRequest()// 任何请求
- .authenticated()// 都需要身份认证
- .and()
- .csrf().disable();
- }
- }
四, 改造代码
即支持表单提交跳转也支持 json 返回, 让用户可以通过自己的配置决定使用哪一种
声明枚举类: LoginType:
- package com.hcx.security.core.properties;
- public enum LoginType {
- REDIRECT,
- JSON
- }
BrowserProperties 中配置:
- package com.hcx.security.core.properties;
- /**
- * @author HCX
- *
- */
- public class BrowserProperties {
/**
* 如果用户配置了就使用用户配置的;
* 如果没有配, 则使用 / hcx-signIn.html
*/
- private String loginPage = "/hcx-signIn.html";// 指定默认跳转
- // 配置默认返回 json
- 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;
- }
- }
- HCXAuthenticationSuccessHandler:
- package com.hcx.security.browser.authentication;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.hcx.security.core.properties.LoginType;
- import com.hcx.security.core.properties.SecurityProperties;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
- import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
- import org.springframework.stereotype.Component;
- @Component("hcxAuthenticationSuccessHandler")
- public class HCXAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Autowired
- private ObjectMapper objectMapper;
- @Autowired
- private SecurityProperties securityProperties;
- // 登录成功后会被调用
/**
* Authentication: 封装认证信息: 包括发起的认证请求的信息, 比如 IP session 和用户信息等
*/
- @Override
- public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
- 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);
- }
- }
- }
- HCXAuthencationFailHandler:
- package com.hcx.security.browser.authentication;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.HttpStatus;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.web.authentication.AuthenticationFailureHandler;
- import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
- import org.springframework.stereotype.Component;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.hcx.security.core.properties.LoginType;
- import com.hcx.security.core.properties.SecurityProperties;
- @Component("hcxAuthenticationFailHandler")
- public class HCXAuthencationFailHandler extends SimpleUrlAuthenticationFailureHandler {
- private Logger logger = LoggerFactory.getLogger(getClass());
- @Autowired
- private ObjectMapper objectMapper;
- @Autowired
- private SecurityProperties securityProperties;
- /**
- * AuthenticationException: 认证过程中发生错误产生异常的信息
- */
- @Override
- public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
- AuthenticationException exception) throws IOException, ServletException {
- logger.info("登录失败");
- if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
- 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);
- }
- }
- }
修改配置来决定使用哪一种方式 (在 BrowserProperties 配置中配置了默认使用 json 返回): 修改 demo 项目中的配置文件 application.properties
修改配置文件决定使用哪种方式响应. png
登录成功跳转页. png
springsecurity 认证执行流程:
springsecurity 认证执行流程. png
认证结果在多个请求之间共享:
认证结果在多个请求之间共享. png
过滤器链. png
获取认证用户信息:
在 UserController 中添加获取用户认证信息:
- @GetMapping("/me")
- public Object getCurrentUser() {
- return SecurityContextHolder.getContext().getAuthentication();
- }
或者直接:
- @GetMapping("/me")
- public Object getCurrentUser(Authentication authentication) {
- return authentication;
- }
或只获取具体某一部分信息:
- @GetMapping("/me")
- public Object getCurrentUser(@AuthenticationPrincipal UserDetails user) {
- return user;
- }
来源: http://www.jianshu.com/p/a5fe18214287