springboot+security 使用说明

it2023-06-18  71

源码

springboot-security

一,简单启用Spring Security

1,引入jar

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

2,配置

application.yml 文件

security: basic: enabled: true @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 .and() .authorizeRequests() // 授权配置 .anyRequest() // 所有请求 .authenticated(); // 都需要认证 } }

3,简单controller

import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author qizenan * @date 2020/10/08 13:12 */ @RestController @RequestMapping("") public class IndexController extends BaseController { @GetMapping(value = "") public ResponseEntity helloWord() { return success("hello world"); } }

4,启用项目

访问 http://127.0.0.1:8081/ username 默认是 user ,password 可以在console的日志中看到,如上= a4a66d19-c1d6-4474-8b2b-00c578134d96

二,自定义用户登录

1,实现 org.springframework.security.core.userdetails.UserDetailsService

import com.springboot.security.po.User; import com.springboot.security.service.UserService; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import javax.annotation.Resource; /** * @author qizenan */ @Configuration public class MyUserDetailService implements UserDetailsService { @Resource private UserService userService; /** * @param username 用户名称 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //userService.getUserByName 根据“用户账号”从数据库中查询用户信息 User user = userService.getUserByName(username); if (user == null) { throw new UsernameNotFoundException("用户" + username + "不存在"); } MyUserDetail userDetails = new MyUserDetail(); userDetails.setUser(user); return userDetails; } @Setter @Getter public class MyUserDetail implements UserDetails { /** * 用户信息 */ private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getName(); } //判读用户是否过期:true 未过期,false 已过期 @Override public boolean isAccountNonExpired() { return true; } //判读用户是否锁定:true 未锁定,false 已锁定 @Override public boolean isAccountNonLocked() { return true; } //判读凭证是否过期:true 未过期,false 已过期 @Override public boolean isCredentialsNonExpired() { return true; } //判读用户是否启用:true 启用,false 未启用 @Override public boolean isEnabled() { return user.getEnabledStatus().equals(UserEnabledStatusEnum.ENABLE.getCode()); } }

2,必修添加密码加密工具,否则报错

在上面的 BrowserSecurityConfig 添加以下配置:

@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ...... }

3,指定自定义的用户登录页面

@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/login.html") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*//**", "/v2/api-docs", "/login.html", "/login.html", "/css/**").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable(); } }

login.html 页面

4,优化拦截登录:如果是html页面则跳转到登录页,如果是接口访问则返回JSON数据,提示登录

import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; 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.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @author qizenan */ @Slf4j @RestController public class BrowserSecurityController { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @GetMapping("/authentication/login") public Map<String, String> loginPage(HttpServletRequest request, HttpServletResponse response) { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { String targetUrl = savedRequest.getRedirectUrl(); if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) { try { redirectStrategy.sendRedirect(request, response, "/login.html"); } catch (IOException e) { log.error("页面跳转异常", e); } } } Map<String, String> data = new HashMap<>(); data.put("code", "500"); data.put("message", "请先登录"); return data; } }

修改配置

@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/authentication/login") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*//**", "/v2/api-docs", "/authentication/login", "/login.html", "/css/**").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable(); }

运行结果

5,默认登录成功后是进入上一个访问请求。如果要修改,则继承 org.springframework.security.web.authentication.AuthenticationSuccessHandler

import org.springframework.security.core.Authentication; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl()); } else { redirectStrategy.sendRedirect(request, response, "/"); } } } @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private MyAuthenticationSucessHandler myAuthenticationSucessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/authentication/login") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") // 指定登录成功处理器 .successHandler(myAuthenticationSucessHandler) .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*//**", "/v2/api-docs", "/authentication/login", "/login.html", "/css/**").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable(); } }

6,修改登录失败默认效果,继承 org.springframework.security.web.authentication.AuthenticationFailureHandler

import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Slf4j @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { Map<String, String> data = new HashMap<>(); data.put("code", "501"); data.put("message", exception.getMessage()); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(data.toString()); } } @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/authentication/login") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") // 指定登录失败处理器 .failureHandler(myAuthenticationFailureHandler) .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*//**", "/v2/api-docs", "/authentication/login", "/login.html", "/css/**").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable(); } }

三,记住登录我

spring security 的记住很简单。当用户勾选“记住”时,sping security 登录成功时会生成token并把token保存到数据库中,之后会把与token关联的cookie返回前端。用户下次访问时,如果cookie没有失效,spring security 根据cookie的值从数据库中查询token,根据token再次登录。

1,配置保存token的数据库

如果有数据库可以不用配置

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://192.168.16.128:3306/ds?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&serverTimezone=UTC

引用数据库jar包

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

2,配置token持久化

@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private DataSource dataSource; /** * 配置"记住我"的token 持久化 */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); //数据源 jdbcTokenRepository.setDataSource(dataSource); //项目启动是是否自动创建token表:true 自动创建,false 需要用户手动创建 jdbcTokenRepository.setCreateTableOnStartup(false); return jdbcTokenRepository; } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/authentication/login") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") // 指定登录成功处理器 .successHandler(myAuthenticationSucessHandler) .and() .rememberMe() // 配置 token 持久化仓库 .tokenRepository(persistentTokenRepository()) // remember 过期时间,单为秒 .tokenValiditySeconds(3600) // 处理自动登录逻辑 .userDetailsService(myUserDetailService) .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*//**", "/v2/api-docs", "/authentication/login", "/login.html", "/css/**", "/logout").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable(); } }

jdbcTokenRepository.setCreateTableOnStartup(true); //项目启动是是否自动创建token表:true 自动创建,false 需要用户手动创建,创建sql可以看源代码 org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl#initDao

3,前端加“记住我”,参数名必修是 remember-me

<input type="checkbox" name="remember-me"/> 记住我

登录页面: 数据记录:

四,添加图形验证

1,生成图片

创建ImageCode

import lombok.Getter; import java.awt.image.BufferedImage; import java.time.LocalDateTime; @Getter public class ImageCode { public final static String SESSION_KEY = "image_code"; /** * 图形验证码图片 */ private BufferedImage image; /** * 图形验证码文字 */ private String code; /** * 过期时间 */ private LocalDateTime expireTime; /** * @param code 图形验证码文字 * @param image 图形验证码图片 * @param expireIn 过期时效,单位秒 */ public ImageCode(String code, BufferedImage image, int expireIn) { this.code = code; this.image = image; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } }

引入jar包

<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>

配置kaptcha

import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; @Configuration public class kaptchaConfiguration { @Bean public Producer createProducer() { DefaultKaptcha kaptcha = new DefaultKaptcha(); Properties properties = new Properties(); properties.setProperty("kaptcha.border", "yes"); properties.setProperty("kaptcha.border.color", "105,179,90"); properties.setProperty("kaptcha.textproducer.font.color", "blue"); properties.setProperty("kaptcha.image.width", "100"); properties.setProperty("kaptcha.image.height", "50"); properties.setProperty("kaptcha.textproducer.font.size", "27"); properties.setProperty("kaptcha.session.key", "code"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCEFGHIJKLMNOPQRSTUVWXYZ"); properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple"); properties.setProperty("kaptcha.noise.color", "black"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise"); properties.setProperty("kaptcha.background.clear.from", "185,56,213"); properties.setProperty("kaptcha.background.clear.to", "white"); properties.setProperty("kaptcha.textproducer.char.space", "3"); kaptcha.setConfig(new Config(properties)); return kaptcha; } }

生成图片验证码

import com.google.code.kaptcha.Producer; import com.springboot.security.config.verification.ImageCode; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; @RestController public class CodeController { @Resource private Producer producer; @RequestMapping("/image/code") public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception { response.setDateHeader("Expires", 0); response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); response.setContentType("image/jpeg"); String code = producer.createText(); BufferedImage image = producer.createImage(code); ImageCode imageCode = new ImageCode(code, image, 300); request.getSession().setAttribute(ImageCode.SESSION_KEY, imageCode); ServletOutputStream out = response.getOutputStream(); ImageIO.write(imageCode.getImage(), "jpg", out); try { out.flush(); } finally { out.close(); } } }

前端添加图片校验

<table> <tr> <td width="48%"><input name="imageCode" placeholder="验证码" style="width: 100%;" type="text"/></td> <td width="48%"><img src="/image/code"/></td> </tr> </table>

2,校验图片

新建一个图片异常

import org.springframework.security.core.AuthenticationException; public class ImageCodeException extends AuthenticationException { public ImageCodeException(String msg) { super(msg); } }

新建图片校验过滤器

import com.springboot.security.config.security.MyAuthenticationFailureHandler; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.annotation.Resource; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.LocalDateTime; @Component public class ImageCodeFilter extends OncePerRequestFilter { @Resource private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if ("/login".equals(request.getServletPath())) { try { validateCode(request); } catch (AuthenticationException e) { myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e); return; } } filterChain.doFilter(request, response); } private void validateCode(HttpServletRequest request) throws ServletRequestBindingException { String imageCodeRequest = ServletRequestUtils.getStringParameter(request, "imageCode"); Object imageCodeSession = request.getSession().getAttribute(ImageCode.SESSION_KEY); if (StringUtils.isBlank(imageCodeRequest)) { throw new ImageCodeException("图片验证码不能为空!"); } if (imageCodeSession == null) { throw new ImageCodeException("图片验证码不存在!"); } ImageCode imageCode = (ImageCode) imageCodeSession; if (LocalDateTime.now().isAfter(imageCode.getExpireTime())) { throw new ImageCodeException("图片验证码已过期!"); } if (!StringUtils.equalsIgnoreCase(imageCodeRequest, imageCode.getCode())) { throw new ImageCodeException("图片验证码错误!"); } request.getSession().removeAttribute(ImageCode.SESSION_KEY); } }

3,生效配置

@Override protected void configure(HttpSecurity http) throws Exception { http //图片验证码过滤器 .addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/authentication/login") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") // 指定登录成功处理器 .successHandler(myAuthenticationSucessHandler) // 指定登录失败处理器 .failureHandler(myAuthenticationFailureHandler) .and() .rememberMe() // 配置 token 持久化仓库 .tokenRepository(persistentTokenRepository()) // remember 过期时间,单为秒 .tokenValiditySeconds(3600) // 处理自动登录逻辑 .userDetailsService(myUserDetailService) .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*/**", "/v2/api-docs", "/webjars/springfox-swagger*/**", "/authentication/login", "static/**", "/login.html", "/css/**", "/logout", "/image/code").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable(); }

http.addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) 把图片过滤器添加到UsernamePasswordAuthenticationFilter之前使其生效,antMatchers 添加 “/image/code” 使其不被拦截。

五,添加新的登录方式:手机号登录

1,发送短信验证码

新建 SmsCode 保存短信验证码

import lombok.Getter; import java.time.LocalDateTime; @Getter public class SmsCode { public final static String SESSION_KEY = "sms_code_"; /** * 手机号 */ private String mobile; /** * 手机号验证码 */ private String code; /** * 过期时间 */ private LocalDateTime expireTime; /** * @param mobile 手机号 * @param code 手机号验证码 * @param expireIn 过期时效,单位秒 */ public SmsCode(String mobile, String code, Integer expireIn) { this.mobile = mobile; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } }

发生短信

@Slf4j @Api(tags = "验证码") @RestController public class CodeController { @Resource private Producer producer; @ApiOperation(value = "手机验证码") @RequestMapping("/sms/code") public void getSmsCode(@ApiIgnore HttpServletRequest request, @ApiParam(value = "手机号") @RequestParam String mobile) { String code = RandomUtils.nextLong(1000, 9999) + ""; log.info("手机验证码 {} ", code); SmsCode smsCode = new SmsCode(mobile, code, 300); request.getSession().setAttribute(SmsCode.SESSION_KEY + mobile, smsCode); } }

2,校验短信验证码

新建 SmsCodeException 短信验证码自己的异常

import org.springframework.security.core.AuthenticationException; public class SmsCodeException extends AuthenticationException { public SmsCodeException(String msg) { super(msg); } }

新建校验短信验证码

import com.springboot.security.config.security.MyAuthenticationFailureHandler; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.annotation.Resource; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.LocalDateTime; @Component public class SmsCodeFilter extends OncePerRequestFilter { @Resource private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if ("/login/sms".equals(request.getServletPath())) { try { validateCode(request); } catch (AuthenticationException e) { myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e); return; } } filterChain.doFilter(request, response); } private void validateCode(HttpServletRequest request) throws ServletRequestBindingException { String smsCodeRequest = ServletRequestUtils.getStringParameter(request, "smsCode"); String mobile = ServletRequestUtils.getStringParameter(request, "mobile"); String smsCodeKey = SmsCode.SESSION_KEY + mobile; Object smsCodeSession = request.getSession().getAttribute(smsCodeKey); if (StringUtils.isBlank(mobile)) { throw new SmsCodeException("手机号不能为空!"); } if (StringUtils.isBlank(smsCodeRequest)) { throw new SmsCodeException("短信验证码不能为空!"); } if (smsCodeSession == null) { throw new SmsCodeException("短信验证码不存在!"); } SmsCode smsCode = (SmsCode) smsCodeSession; if (LocalDateTime.now().isAfter(smsCode.getExpireTime())) { throw new SmsCodeException("短信验证码已过期!"); } if (!StringUtils.equalsIgnoreCase(smsCodeRequest, smsCode.getCode())) { throw new SmsCodeException("短信验证码错误!"); } request.getSession().removeAttribute(smsCodeKey); } }

3,手机号登录

用户密码登录的顺序是 UsernamePasswordAuthenticationFilter -> ProviderManager -> UsernamePasswordAuthenticationToken -> AbstractUserDetailsAuthenticationProvider -> UserDetailService 。我们仿照 UsernamePassword 创建 手机号登录

复制 UsernamePasswordAuthenticationFilter 新建 SmsAuthenticationFilter

import org.springframework.lang.Nullable; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String mobile = "mobile"; private boolean postOnly = true; public SmsAuthenticationFilter() { super(new AntPathRequestMatcher("/login/sms", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String mobile = this.obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } @Nullable protected String obtainMobile(HttpServletRequest request) { return request.getParameter(this.mobile); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } } import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 530L; private final Object principal; public SmsCodeAuthenticationToken(Object principal) { super(null); this.principal = principal; this.setAuthenticated(false); } public SmsCodeAuthenticationToken(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"); } else { super.setAuthenticated(false); } } @Override public void eraseCredentials() { super.eraseCredentials(); } }

复制 AbstractUserDetailsAuthenticationProvider 新建 SmsAuthenticationProvider

import lombok.Setter; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.util.Assert; @Setter public class SmsAuthenticationProvider implements AuthenticationProvider, InitializingBean { private UserDetailsService userDetailsService; private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); public SmsAuthenticationProvider() { } @Override public void afterPropertiesSet() { Assert.notNull(this.userDetailsService, "userDetailsService must be set"); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String mobile = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); UserDetails user = userDetailsService.loadUserByUsername(mobile); return this.createSuccessAuthentication(user, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(principal, this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } }

public boolean supports(Class<?> authentication) 需要重写,把 SmsCodeAuthenticationToken 与 SmsAuthenticationProvider 关联上

新建手机号的 UserDetailService

import com.springboot.security.config.security.MyUserDetail; import com.springboot.security.po.User; import com.springboot.security.service.UserService; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import javax.annotation.Resource; /** * @author qizenan */ @Configuration public class SmsUserDetailService implements UserDetailsService { @Resource private UserService userService; /** * @param mobile 手机号 */ @Override public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { //根据手机号查询数据库 User user = userService.getUserByMobile(mobile); if (user == null) { throw new UsernameNotFoundException("用户手机号" + mobile + "不存在"); } MyUserDetail userDetails = new MyUserDetail(); userDetails.setUser(user); return userDetails; } }

把手机验证码的 SmsAuthenticationFilter,SmsAuthenticationProvider,SmsUserDetailService 关联起来

import com.springboot.security.config.security.MyAuthenticationFailureHandler; import com.springboot.security.config.security.MyAuthenticationSucessHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.stereotype.Component; @Component public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private MyAuthenticationSucessHandler myAuthenticationSucessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private SmsUserDetailService smsUserDetailService; @Override public void configure(HttpSecurity http) { SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSucessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler); http.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public SmsAuthenticationProvider smsAuthenticationProvider() { SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); smsAuthenticationProvider.setUserDetailsService(smsUserDetailService); return smsAuthenticationProvider; } }

这样整个手机号登录的流程配置好了 SmsAuthenticationFilter-> ProviderManager -> SmsCodeAuthenticationToken -> SmsAuthenticationProvider -> SmsUserDetailService

4,配置生效

@Resource private SmsCodeFilter smsCodeFilter; @Resource private SmsAuthenticationConfig smsAuthenticationConfig; /** * spring security 默认的 DaoAuthenticationProvider */ @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); daoAuthenticationProvider.setUserDetailsService(myUserDetailService); return daoAuthenticationProvider; } /** * 配置多个Provider: daoAuthenticationProvider , smsAuthenticationProvider */ @Override protected AuthenticationManager authenticationManager() { ProviderManager authenticationManager = new ProviderManager(Arrays.asList( daoAuthenticationProvider(), smsAuthenticationConfig.smsAuthenticationProvider())); authenticationManager.setEraseCredentialsAfterAuthentication(false); return authenticationManager; } @Override protected void configure(HttpSecurity http) throws Exception { http //图片验证码过滤器 .addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) //短信验证码过滤器 .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() // 表单登录 // 指定了跳转到登录页面的请求UR .loginPage("/authentication/login") // 对应登录页面form表单的action="/login" .loginProcessingUrl("/login") // 指定登录成功处理器 .successHandler(myAuthenticationSucessHandler) // 指定登录失败处理器 .failureHandler(myAuthenticationFailureHandler) .and() .logout() //自定义退出成功的url .logoutSuccessUrl("/index.html") .and() .rememberMe() // 配置 token 持久化仓库 .tokenRepository(persistentTokenRepository()) // remember 过期时间,单为秒 .tokenValiditySeconds(3600) // 处理自动登录逻辑 .userDetailsService(myUserDetailService) .and() .authorizeRequests() // 授权配置 .antMatchers("/swagger*/**", "/v2/api-docs", "/webjars/springfox-swagger*/**", "/authentication/login", "static/**", "/login.html", "/css/**", "/logout", "/image/code", "/loginSms.html", "/sms/code", "/login/sms").permitAll() .anyRequest() // 所有请求 .authenticated() // 都需要认证 .and().csrf().disable() // 将短信验证码认证配置加到 Spring Security 中; .apply(smsAuthenticationConfig); }

antMatchers 放行请求 “/loginSms.html”, “/sms/code”, “/login/sms”。.apply(smsAuthenticationConfig); 使SmsAuthenticationConfig 配置生效。addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) 使 短信验证校验生效。

5,前端代码

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登录</title> <link href="css/login.css" rel="stylesheet" type="text/css"> </head> <body> <form action="/login/sms" class="login-page" method="post"> <div class="form"> <h3>短信验证码登录</h3> <input name="mobile" placeholder="手机号" required="required" type="text" value="17777777777"/> <span style="display: inline"> <input name="smsCode" placeholder="短信验证码" style="width: 50%;" type="text"/> <a href="/sms/code?mobile=17777777777">发送验证码</a> </span> <button type="submit">登录</button> </div> </form> </body> </html>

运行结果

六,自定义退出动作

最新回复(0)