springboot-security
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(); // 都需要认证 } }访问 http://127.0.0.1:8081/ username 默认是 user ,password 可以在console的日志中看到,如上= a4a66d19-c1d6-4474-8b2b-00c578134d96
在上面的 BrowserSecurityConfig 添加以下配置:
@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ...... }login.html 页面
修改配置
@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(); }运行结果
spring security 的记住很简单。当用户勾选“记住”时,sping security 登录成功时会生成token并把token保存到数据库中,之后会把与token关联的cookie返回前端。用户下次访问时,如果cookie没有失效,spring security 根据cookie的值从数据库中查询token,根据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>jdbcTokenRepository.setCreateTableOnStartup(true); //项目启动是是否自动创建token表:true 自动创建,false 需要用户手动创建,创建sql可以看源代码 org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl#initDao
登录页面: 数据记录:
创建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>新建一个图片异常
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); } }http.addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) 把图片过滤器添加到UsernamePasswordAuthenticationFilter之前使其生效,antMatchers 添加 “/image/code” 使其不被拦截。
新建 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); } }新建 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); } }用户密码登录的顺序是 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
antMatchers 放行请求 “/loginSms.html”, “/sms/code”, “/login/sms”。.apply(smsAuthenticationConfig); 使SmsAuthenticationConfig 配置生效。addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) 使 短信验证校验生效。
运行结果