基础翻译篇
简介
- Spring Security 是一个提供身份验证,授权,保护以及防止常见攻击的框架,由于同时支持响应式和命令式,是spring框架的安全标准。
- 前提条件
jdk1.8或者更高的版本 - Spring Security是一个独立的包含所有的独立容器,在java运行环境中不需添加任何特殊的配置文件,尤其是我们不需要配置特殊的Java授权和认证JAAS策略文件,也不需要将Spring Security的信息放入公共的类路径
同样的,如果你使用的是EJB或者Servlet容器,不需要在任意的地方做任何特殊的配置,也不需要在服务类加载容器中包含Spring Security,所有必须的文件都已经包含在了应用程序中
这种设计提供了最大的部署时间灵活性,可以拷贝我们的(jar,war,ear)从一个系统到另一个系统并可以轻松使用 - Spring Security源代码
- 简单demo启动
添加pom
<dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
由于springboot提供 了Maven Bom来管理版本,因此不需制定版本,如果我们需要覆盖Spring Security版本,可以通过提供Maven属性来配置
例
<properties> <!-- ... --> <spring-security.version>5.2.2.BUILD-SNAPSHOT</spring-security.version> </properties>
由于Spring Security在主版本进行重大更改,因此可以使用较新版本的Spring Security与SpringBoot一起使用是比较安全的,但是有时还是需要指定Spring Framework的版本
例
<properties> <spring.version>5.2.1.RELEASE</spring.version> </properties>
- 如果在没有使用SpringBoot的情况下,我们使用Spring Security的BOM来确定SpringSecurity,来确保项目中使用一致的SpringSecurity版本
例
<dependencyManagement> <dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-bom</artifactId> <version>5.2.2.BUILD-SNAPSHOT</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
7.Spring Security最小依赖必须的如下
<dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> </dependencies>
- 所有的RELEASE版本都已经上传到Maven中心仓库,无需在pom中指定其他maven库,如果使用SNAPSHOT版本则需要定义Spring Snapshot存储库
例
<repositories> <!-- ... possibly other repository elements ... --> <repository> <id>spring-snapshot</id> <name>Spring Snapshot Repository</name> <url>https://repo.spring.io/snapshot</url> </repository> </repositories>
9.如果使用里程碑版本或者候选的版本,则需要指定Spring Milestone存储库
<repositories> <!-- ... possibly other repository elements ... --> <repository> <id>spring-milestone</id> <name>Spring Milestone Repository</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories>
认证
Spring Security 支持内置的用户验证,基于用户名密码,用户输入用户名密码访问特定的资源,此时会触发身份验证,进行授权
密码认证
常用密码编码器
- DelegatingPasswordEncoder 委派授权密码编码器
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
- BCryptPasswordEncoder 使用
bcrypt
算法对密码进行hash
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
- Argon2PasswordEncoder 使用
argon2
算法对密码进行hash
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
- Pbkdf2PasswordEncoder 使用
bkdf2
算法 对密码进行hash
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
- SCryptPasswordEncoder 使用
scrypt
算法进行hash
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
DelegatingPasswordEncoder 详解
A password encoder that delegates to another PasswordEncoder based upon a prefixed identifier.
本质上还是使用的另一个密码编码器,只是加了一个前缀来区分使用的是什么类型的密码编码器
此处扩展一下,在 Spring Security 5.0 之前,默认的 PasswordEncoder 是NoOpPasswordEncoder,纯文本密码的对比.现在已经被标记为不安全,使用 DelegatingPasswordEncoder 代替
DelegatingPasswordEncoder 编码格式
{id}encodedPassword
id 就是编码类型标识符,encodedPassword 是原密码经过 PasswordEncoder 编码之后的密码,id 必须在密码的开始,以{id}的形式存在.如果 id 找不到,id 就是 null,以下是使用不同的 编码器对原密码 password
编码之后的结果
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
解读如下
第一个密码的PasswordEncoderID为bcrypt,编码密码为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时将委托给BCryptPasswordEncoder 第二个密码的PasswordEncoderID为noop,编码密码为password。匹配时将委托给NoOpPasswordEncoder 第三个密码的PasswordEncoderID为pbkdf2,编码密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时将委托给Pbkdf2PasswordEncoder 第四个密码的PasswordEncoderID为scrypt且编码密码为,$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 匹配时将委派给SCryptPasswordEncoder 最后密码的PasswordEncoderID为sha256,编码的密码为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时将委托给StandardPasswordEncoder
为什么推荐使用 DelegatingPasswordEncoder?
- 许多应用程序使用旧的密码编码无法轻松迁移
- 密码存储的最佳做法将再次更改。
- 作为一个框架,Spring Security不能经常进行重大更改
可以解决什么问题?
- 使用当前密码推荐的存储建议编码
- 兼容旧密码和新密码
- 允许将来升级编码
自定义创建实例
String idForEncode = "bcrypt"; Map<String,PasswordEncoder> encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
实战篇
SpringBoot 开启 Spring Security
SpringBoot + Spring Security 自定义用户认证 专业篇
项目使用上一篇文章创建 demo,demo 实现自定义用户登录,记住我功能,图片验证码.
通过本文你可以学到什么?
- SpringBoot 如何 集成 Spring Security?
- SpringSecurity 自定义用户认证如何实现? 关键词: 自定义用户认证
- SpringSecurity 怎么实现记住我功能? 关键词: 记住我
- SpringSecurity 怎么实现图片验证码登录拦截? 关键词: 图片验证码
对于以上问题,本篇文章将为你揭开神秘面纱.问题后面关键词为相应问题的代码实现.文章没有单独 拆分出来,代码标记处理.可根据关键词在本文中分解对应关键代码片段
自定义用户登录过程
- 自定义用户认证 创建自定义用户类 MyUser
@Data public class MyUser implements Serializable { private static final long serialVersionUID = 3497935890426858541L; /** * 用户名 */ private String userName; /** * 密码 */ private String password; /** * 账号是否过期 */ private boolean accountNonExpired = true; /** * 账号是否未锁定 */ private boolean accountNonLocked = true; /** * 用户密码凭证是否未过期 */ private boolean credentialsNonExpired = true; /** * 用户账户是否可用 */ private boolean enabled = true; }
- 自定义用户认证 自定义 MyUserDetailsService
@Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { // 模拟一个用户,替代数据库获取逻辑 MyUser user = new MyUser(); user.setUserName(s); user.setPassword(BCrypt.hashpw("123", BCrypt.gensalt())); // 输出加密后的密码 System.out.println(user.getPassword()); // 此处返回一个任何用户名,密码都是 123 的账号 return new User(s, user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("p1,p2")); }
- 返回的 UserDetails 也是一个接口,源码如下
public interface UserDetails extends Serializable { // 获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象; Collection<? extends GrantedAuthority> getAuthorities(); // 获取密码 String getPassword(); // 获取用户名 String getUsername(); // 判断账户是否未过期,未过期返回true反之返回false boolean isAccountNonExpired(); // 判断账户是否未锁定 boolean isAccountNonLocked(); // 判断用户凭证是否没过期,即密码是否未过期 boolean isCredentialsNonExpired(); // 判断用户是否可用 boolean isEnabled(); }
替换默认的登录页面
- **自定义用户认证 ** 记住我 创建简单版本登录页面
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登录</title> <link rel="stylesheet" href="css/login.css" type="text/css"> </head> <body> <form class="login-page" action="/login" method="post"> <div class="form"> <h3>账户登录</h3> <input type="text" placeholder="用户名" name="username" required="required"/> <input type="password" placeholder="密码" name="password" required="required"/> <span style="display: inline"> <input type="text" name="imageCode" placeholder="验证码" style="width: 50%;"/> <img src="/code/image"/> </span> <input type="checkbox" name="remember-me"/> 记住我 <button type="submit">登录</button> </div> </form> </body> </html>
- 自定义用户认证 登录页面样式 css 见 src/main/resources/static/css/login.css
配置登录页面跳转,验证码校验,处理成功失败
- Spring Security 配置
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private DataSource dataSource; @Resource private MyUserDetailService userDetailService; /** * Override this method to configure the {@link HttpSecurity}. Typically subclasses * should not invoke this method by calling super as it may override their * configuration. The default configuration is: * * <pre> * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); * </pre> * * @param http the {@link HttpSecurity} to modify * @throws Exception if an error occurs */ @Override protected void configure(HttpSecurity http) throws Exception { http // **记住我** .rememberMe() .tokenRepository(persistentTokenRepository()) // remember 过期时间,单为秒 .tokenValiditySeconds(3600) // 处理自动登录逻辑 .userDetailsService(userDetailService) .and() // **图片验证码** 添加验证码拦截器在 UsernamePasswordAuthenticationFilter 之前 .addFilterBefore(validateCodeFilter(), UsernamePasswordAuthenticationFilter.class) // **自定义用户认证** 表单登录 .formLogin() // HTTP Basic // . http.httpBasic() // **自定义用户认证** 登录跳转 URL .loginPage("/authentication/require") // **自定义用户认证** 处理表单登录 URL .loginProcessingUrl("/login") // 处理登录成功 .successHandler(myAuthenticationSuccessHandler()) // 处理登录失败 .failureHandler(myAuthenticationFailureHandler()) .and() // **自定义用户认证** 授权配置 .authorizeRequests() // 登录跳转 URL 无需认证 .antMatchers("/authentication/require", "/login.html", "/code/image").permitAll() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2") .antMatchers("/r/**").authenticated() // 所有请求放行 .anyRequest().permitAll() // 都需要认证 // .authenticated() .and().csrf().disable(); } /** * **自定义用户认证** * 密码验证方式 * NoOpPasswordEncoder.getInstance() 字符串校验 * * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 认证成功处理 * @return */ @Bean public MyAuthenticationSuccessHandler myAuthenticationSuccessHandler() { return new MyAuthenticationSuccessHandler(); } /** * 认证失败处理 * @return */ @Bean public MyAuthenticationFailureHandler myAuthenticationFailureHandler() { return new MyAuthenticationFailureHandler(); } /** * **图片验证码** * 验证码过滤器 * @return */ @Bean public ValidateCodeFilter validateCodeFilter() { return new ValidateCodeFilter(); } /** * **记住我** * 记住我持久化使用 * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); jdbcTokenRepository.setCreateTableOnStartup(false); return jdbcTokenRepository; } }
- 自定义用户认证 统一的登录拦截跳转
@RestController public class BrowserSecurityController { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); /** * 其中HttpSessionRequestCache为Spring Security提供的用于缓存请求的对象, * 通过调用它的getRequest方法可以获取到本次请求的HTTP信息。 * DefaultRedirectStrategy的sendRedirect为Spring Security提供的用于处理重定向的方法。 * <p> * 上面代码获取了引发跳转的请求,根据请求是否以.html为结尾来对应不同的处理方法。 * 如果是以.html结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(HttpStatus.UNAUTHORIZED)。 * * @param request * @param response * @return * @throws IOException */ @GetMapping("/authentication/require") @ResponseStatus(HttpStatus.UNAUTHORIZED) public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { String targetUrl = savedRequest.getRedirectUrl(); if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) { redirectStrategy.sendRedirect(request, response, "/login"); } } return "访问的资源需要身份认证!"; } }
处理成功与失败逻辑
- 处理成功的逻辑,对象在 config 中配置
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { /** * json 处理登陆成功逻辑 */ // @Autowired // private ObjectMapper objectMapper; // @Override // public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // httpServletResponse.setContentType("application/json;charset=utf-8"); // httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authentication)); // } /** * 成功页面跳转 */ private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); // savedRequest.getRedirectUrl(); redirectStrategy.sendRedirect(request, response, "/index"); } }
- 处理失败的逻辑
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper mapper; /** * 不同的失败原因对应不同的异常, * 比如用户名或密码错误对应的是BadCredentialsException * 用户不存在对应的是UsernameNotFoundException, * 用户被锁定对应的是LockedException * * @param request * @param response * @param exception * @throws IOException */ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(mapper.writeValueAsString(exception.getMessage())); } }
记住我功能相关
- pom.xml
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
- application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/springboot-security?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username=root spring.datasource.password=root
- 数据库持久化语句,可在这个类中复制org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl#CREATE_TABLE_SQL
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
图片验证码相关
- pom.xml
<!-- https://mvnrepository.com/artifact/org.springframework.social/spring-social-config --> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>1.1.6.RELEASE</version> </dependency>
- ImageCode 类
@Data public class ImageCode { /** * 图片 */ private BufferedImage image; /** * 验证码 */ private String code; /** * 过期时间 */ private LocalDateTime expireTime; public ImageCode(BufferedImage image, String code, int expireIn) { this.image = image; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) { this.image = image; this.code = code; this.expireTime = expireTime; } /** * 校验是否过期 * * @return */ public boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); } }
- 生成验证码
@RestController public class ValidateController { public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE"; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = createImageCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode); ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream()); } private ImageCode createImageCode() { int width = 100; // 验证码图片宽度 int height = 36; // 验证码图片长度 int length = 4; // 验证码位数 int expireIn = 60; // 验证码有效时间 60s BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); Random random = new Random(); g.setColor(getRandColor(200, 250)); g.fillRect(0, 0, width, height); g.setFont(new Font("Times New Roman", Font.ITALIC, 20)); g.setColor(getRandColor(160, 200)); for (int i = 0; i < 155; i++) { int x = random.nextInt(width); int y = random.nextInt(height); int xl = random.nextInt(12); int yl = random.nextInt(12); g.drawLine(x, y, x + xl, y + yl); } StringBuilder sRand = new StringBuilder(); for (int i = 0; i < length; i++) { String rand = String.valueOf(random.nextInt(10)); sRand.append(rand); g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110))); g.drawString(rand, 13 * i + 6, 16); } g.dispose(); return new ImageCode(image, sRand.toString(), expireIn); } private Color getRandColor(int fc, int bc) { Random random = new Random(); if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } }
- 验证码校验 filter
@Component public class ValidateCodeFilter extends OncePerRequestFilter { @Autowired private AuthenticationFailureHandler authenticationFailureHandler; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); /** * Spring Security实际上是由许多过滤器组成的过滤器链, * 处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter, * 而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后采去校验用户名和密码。 * 由于Spring Security并没有直接提供验证码校验相关的过滤器接口, * 所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter: * 在doFilterInternal方法中我们判断了请求URL是否为/login, * 该路径对应登录form表单的action路径,请求的方法是否为POST,是的话进行验证码校验逻辑, * 否则直接执行filterChain.doFilter让代码往下走。 * 当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处理器AuthenticationFailureHandler进行处理。 * * @param httpServletRequest * @param httpServletResponse * @param filterChain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI()) && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) { try { validateCode(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } /** * 我们分别从Session中获取了ImageCode对象和请求参数imageCode(对应登录页面的验证码<input>框name属性), * 然后进行了各种判断并抛出相应的异常。 * 当验证码过期或者验证码校验通过时,我们便可以删除Session中的ImageCode属性了。 * * @param servletWebRequest * @throws ServletRequestBindingException */ private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode"); if (StringUtils.isBlank(codeInRequest)) { throw new ValidateCodeException("验证码不能为空!"); } if (codeInSession == null) { throw new ValidateCodeException("验证码不存在!"); } if (codeInSession.isExpire()) { sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); throw new ValidateCodeException("验证码已过期!"); } if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) { throw new ValidateCodeException("验证码不正确!"); } sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE); } }
- 异常处理
public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String message) { super(message); } }
到了这里 代码基本分享 完毕 了.具体的效果等你来亲自验证了
Spring Security 加入短信验证码
只做代码记录,具体的可下载代码之后查看,源码点击原文或者下方源码链接
- 定义成功响应
@Component public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { redirectStrategy.sendRedirect(request, response, "/index"); } }
- 定义失败响应
@Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper mapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(mapper.writeValueAsString(exception.getMessage())); } }
- 配置验证码拦截器
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String MOBILE_KEY = "mobile"; private String mobileParameter = MOBILE_KEY; private boolean postOnly = true; public SmsAuthenticationFilter() { super(new AntPathRequestMatcher("/login/mobile", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String mobileParameter) { Assert.hasText(mobileParameter, "mobile parameter must not be empty or null"); this.mobileParameter = mobileParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } }
- 自定义短信验证码 token
public class SmsAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; public SmsAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
- 配置 验证码的配置
@Component public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private UserDetailService userDetailService; @Override public void configure(HttpSecurity http) throws Exception { SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); smsAuthenticationProvider.setUserDetailService(userDetailService); http.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
- token的提供认证 SmsAuthenticationProvider
public class SmsAuthenticationProvider implements AuthenticationProvider { private UserDetailService userDetailService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (userDetails == null) throw new InternalAuthenticationServiceException("未找到与该手机号对应的用户"); SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class<?> aClass) { return SmsAuthenticationToken.class.isAssignableFrom(aClass); } public UserDetailService getUserDetailService() { return userDetailService; } public void setUserDetailService(UserDetailService userDetailService) { this.userDetailService = userDetailService; } }
- 验证码 model
public class SmsCode { private String code; private LocalDateTime expireTime; public SmsCode(String code, int expireIn) { this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } public SmsCode(String code, LocalDateTime expireTime) { this.code = code; this.expireTime = expireTime; } boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public LocalDateTime getExpireTime() { return expireTime; } public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; } }
- 自定义验证码异常
public class ValidateCodeException extends AuthenticationException { private static final long serialVersionUID = 5022575393500654458L; public ValidateCodeException(String message) { super(message); } }
- 最关键的安全配置,让短信拦截加入我们的拦截链
@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private MyAuthenticationFailureHandler authenticationFailureHandler; @Autowired private SmsCodeFilter smsCodeFilter; @Autowired private SmsAuthenticationConfig smsAuthenticationConfig; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/login") .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler) .and() .authorizeRequests() .antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms", "/css/**").permitAll() .anyRequest() .authenticated() .and() .csrf().disable() .apply(smsAuthenticationConfig); } }
- 定义登录页面
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登录</title> <link rel="stylesheet" href="css/login.css" type="text/css"> </head> <body> <form class="login-page" action="/login/mobile" method="post"> <div class="form"> <h3>短信验证码登录</h3> <input type="text" placeholder="手机号" name="mobile" value="17777777777" required="required"/> <span style="display: inline"> <input type="text" name="smsCode" placeholder="短信验证码" style="width: 50%;"/> <a href="/code/sms?mobile=17777777777">发送验证码</a> </span> <button type="submit">登录</button> </div> </form> </body> </html>
- 配置登录页面拦截 404
@Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); registry.addViewController("/login.html").setViewName("login"); } }
- 到这代码就可以运行了,效果就不演示了,流程 debug 一下就知道流程了哈哈哈哈哈,不明白也可以留言