/** * 前往手机验证码登录页 * * @return */ @RequestMapping("/mobile/page") public String toMobilePage() { return "login-mobile"; // templates/login-mobile.html }
/** * 资源权限配置(过滤器链): * 1、被拦截的资源 * 2、资源所对应的角色权限 * 3、定义认证方式:httpBasic 、httpForm * 4、定制登录页面、登录请求地址、错误处理方式 * 5、自定义 spring security 过滤器 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //http.httpBasic()//采用httpBasic 认证方式 /*http.formLogin() .loginPage("/login/page")// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl("/login/form") // 登录表单提交处理Url, 默认是 /login .usernameParameter("name") // 默认用户名的属性名是 username .passwordParameter("pwd") // 默认密码的属性名是 password .and() .authorizeRequests()//认证请求 .antMatchers("/login/page").permitAll()//自定义登录页不需要认证 .anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证*/ http .addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .formLogin() .loginPage(securityProperties.getLoginPage())// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl(securityProperties.getLoginProcessingUrl()) // 登录表单提交处理Url, 默认是 /login .usernameParameter(securityProperties.getUsernameParameter()) // 默认用户名的属性名是 username .passwordParameter(securityProperties.getPasswordParameter()) // 默认密码的属性名是 password .successHandler(customAuthenticationSuccessHandler)//自定义认证成功处理器 .failureHandler(customAuthenticationFailureHandler)//自定义认证失败处理器 .and() .authorizeRequests()//认证请求 .antMatchers(securityProperties.getLoginPage(),securityProperties.getMobilePage(),securityProperties.getImageCodeUrl(),securityProperties.getMobileCodeUrl()).permitAll()//自定义登录页不需要认证,生成图片验证码,发送短信获取验证码也不需要验证 .anyRequest().authenticated()// 所有进入应用的HTTP请求都要进行认证 .and() .rememberMe()//记住我功能 .tokenRepository(jdbcTokenRepository())//保存登录信息 .tokenValiditySeconds(securityProperties.getTokenValiditySeconds());//记住我有效时长一周 // 将手机相关的配置绑定过滤器链上 http.apply(mobileAuthenticationConfig); }
上述自己去阿里云短信服务模块去申请
@Component @ConfigurationProperties(prefix = "moblie.sms") @Data public class SmsProperties { private String accessKeyId; private String accessKeySecret; private String signName; private String templateCode; private String product; private String domain; }
@Controller public class SmsSendController { @Autowired SmsSendService smsSendService; @RequestMapping(value = "/code/mobile",method = RequestMethod.GET) @ResponseBody public Result smsCodeSend(@RequestParam("mobile") String phoneNumbers, HttpServletRequest request){ return smsSendService.sendSms(phoneNumbers,request); } }
/** * 短信发送接口 */ public interface SmsSendService { /** * 短信发送 * @param PhoneNumbers 手机号 * @return */ Result sendSms(String PhoneNumbers, HttpServletRequest request); }
/** * 发送短信验证码 */ @Service public class SmsSendServiceImpl implements SmsSendService { Logger logger= LoggerFactory.getLogger(SmsSendServiceImpl.class); public static final String SESSION_SMS_VERIFY_CODE = "SESSION_SMS_VERIFY_CODE"; @Autowired SmsProperties smsProperties; @Override public Result sendSms(String PhoneNumbers, HttpServletRequest request) { //1.生成一个手机短信验证码 String code = RandomUtil.randomNumbers(4); //2.将验证码发到session中 HttpSession session = request.getSession(); session.setAttribute(SESSION_SMS_VERIFY_CODE,code); //3.发送短信 SendSmsResponse response = toSendSms(PhoneNumbers, code); logger.info("向手机号" + PhoneNumbers + "发送的验证码为::" + code); if (response.getCode().equals("ok")){ return Result.ok("获取验证码成功"); }else { return Result.build(500,"获取验证码失败"); } } public SendSmsResponse toSendSms(String PhoneNumbers, String code) { //可自助调整超时时间 System.setProperty("sun.net.client.defaultConnectTimeout", "10000"); System.setProperty("sun.net.client.defaultReadTimeout", "10000"); //初始化acsClient,暂不支持region化 IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret()); DefaultProfile.addEndpoint( "cn-hangzhou", smsProperties.getProduct(), smsProperties.getDomain()); IAcsClient acsClient = new DefaultAcsClient(profile); //组装请求对象-具体描述见控制台-文档部分内容 SendSmsRequest request = new SendSmsRequest(); //必填:待发送手机号 request.setPhoneNumbers(PhoneNumbers); //必填:短信签名-可在短信控制台中找到 request.setSignName(smsProperties.getSignName()); //必填:短信模板-可在短信控制台中找到 request.setTemplateCode(smsProperties.getTemplateCode()); //可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为 request.setTemplateParam("{\"code\":\"" + code + "\"}"); //选填-上行短信扩展码(无特殊需求用户请忽略此字段) //request.setSmsUpExtendCode("90997"); //可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者 request.setOutId("yourOutId"); //hint 此处可能会抛出异常,注意catch SendSmsResponse sendSmsResponse = null; try { sendSmsResponse = acsClient.getAcsResponse(request); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("发送短信失败"); } return sendSmsResponse; } }
4、实现短信验证码校验过滤器 SmsVerifyCodeValidateFilter,校验输入的验证码与发送的短信验证是否一致。
/** * * 短信验证码校验过滤器 * OncePerRequestFilter: 所有请求之前被调用一次 */ @Component("smsVerifyCodeValidateFilter") public class SmsVerifyCodeValidateFilter extends OncePerRequestFilter { @Autowired SecurityProperties securityProperties; @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //1.判断手机登录认证 且请求方式为post if (securityProperties.getMobileProcessingUrl().equalsIgnoreCase(request.getRequestURI())&&request.getMethod().equalsIgnoreCase("post")){ try { //校验验证码合法性 validate(request); } catch (AuthenticationException e) { //将验证失败的抛出的异常交给自定义认证失败处理器处理异常 customAuthenticationFailureHandler.onAuthenticationFailure(request,response,e); return; } } filterChain.doFilter(request,response); } private void validate(HttpServletRequest request) { //先获取session中的验证码 String sessionImageCode = (String) request.getSession().getAttribute(SmsSendServiceImpl.SESSION_SMS_VERIFY_CODE); //获取用户输入的验证码 String inputCode = request.getParameter("code"); if (StringUtils.isBlank(inputCode)){ throw new SmsVerifyCodeException("验证码不能为空"); } if (!inputCode.equalsIgnoreCase(sessionImageCode)){ throw new SmsVerifyCodeException("验证码输入错误"); } } }
5、实现手机认证登录过滤器MobileAuthenticationFilter,模仿UsernamePasswordAuthenticationFilter进行改造
** * 手机登录过滤器 * 实现同UsernamePasswordAuthenticationFilter * 将username相关的都改成mobile,而且手机登录只有手机号,没有密码,所以去掉密码 * 相应的参数最好写成可配置的 * */ public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== @Resource SecurityProperties securityProperties; /** * 前端表单中的手机号码参数 */ private String mobileParameter = "mobile"; private boolean postOnly = true; // ~ Constructors // =================================================================================================== public MobileAuthenticationFilter() { super(new AntPathRequestMatcher("/mobile/form", "POST")); } // ~ Methods // ======================================================================================================== 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(); MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * Enables subclasses to override the composition of the username, such as by * including additional values and a separator. * * @param request so that request attributes can be retrieved * @return the username that will be presented in the <code>Authentication</code> * request token to the <code>AuthenticationManager</code> */ @Nullable protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } /** * Provided so that subclasses may configure what is put into the authentication * request's details property. * * @param request that an authentication request is being created for * @param authRequest the authentication request object that should have its details * set */ protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * Defines whether only HTTP POST requests will be allowed by this filter. If set to * true, and an authentication request is received which is not a POST request, an * exception will be raised immediately and authentication will not be attempted. The * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed * authentication. * <p> * Defaults to <tt>true</tt> but may be overridden by subclasses. */ public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public String getMobileParameter() { return mobileParameter; } public void setMobileParameter(String mobileParameter) { this.mobileParameter = mobileParameter; } }
6、封装手机认证Token MobileAuthenticationToken提供给上面自定义的 MobileAuthenticationFilter 使用。模仿UsernamePasswordAuthenticationToken进行改造
/** * 封装手机认证Token * 实现同UsernamePasswordAuthenticationToken */ public class MobileAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; //认证之前存放机号,认证之后放用户信息 private final Object principal; /** * 开始认证时,创建一个MobileAuthenticationToken实例 接收的是手机号码, 并且 标识未认证 * * @param principal 手机号 */ public MobileAuthenticationToken(Object principal) { super(null); this.principal = principal; // 手机号 setAuthenticated(false); } /** * 当认证通过后,会重新创建一个新的MobileAuthenticationToken,来标识它已经认证通过, * * @param principal 用户信息 * @param authorities 用户权限 */ public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal;// 用户信息 super.setAuthenticated(true); // 标识已经认证通过 } @Override public Object getCredentials() { return null; } public Object getPrincipal() { return this.principal; } 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(); } }
7、实现手机认证提供者 MobileAuthenticationProvider,提供给底层 ProviderManager 使用。
/** * 实现手机认证提供者 MobileAuthenticationProvider提供给底层 ProviderManager 使用 */ public class MobileAuthenticationProvider implements AuthenticationProvider { UserDetailsService userDetailsService; public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } /** * 认证处理: * 1. 通过 手机号 去数据库查询用户信息(UserDeatilsService) * 2. 再重新构建认证信息 * * @param authentication * @return * @throws AuthenticationException */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication; // 获取用户输入的手机号 String mobile = (String) mobileAuthenticationToken.getPrincipal(); // 查询数据库 UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); //未查询到用户信息 if (userDetails == null) { throw new AuthenticationServiceException("该手机未注册"); } // 查询到了用户信息, 则认证通过,就重新构建 MobileAuthenticationToken 实例 MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationToken.setDetails(mobileAuthenticationToken.getDetails()); return authenticationToken; } /** * 通过此方法,来判断 采用哪一个 AuthenticationProvider * * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return MobileAuthenticationToken.class.isAssignableFrom(authentication); } }
8、手机号获取用户信息 MobileUserDetailsService
/** * 通过手机号获取用户信息和权限信息 */ @Component("mobileUserDetailsService") public class MobileUserDetailsService implements UserDetailsService { Logger logger = LoggerFactory.getLogger(MobileUserDetailsService.class); @Override public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { logger.info("请求的手机号是:" + mobile); // 1. 通过手机号查询用户信息(查询数据库) // 2. 如果有此用户,则查询用户权限 // 3. 封装用户信息 return new User(mobile, "", true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN")); } }
9、自定义管理认证配置 MobileAuthenticationConfig,将上面定义的组件绑定起来,添加到容器中
/** * 自定义管理认证配置 * 将定义的手机短信认证相关的组件组合起来,一起添加到容器中 */ @Component("mobileAuthenticationConfig") public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Autowired MobileUserDetailsService mobileUserDetailsService; @Override public void configure(HttpSecurity http) throws Exception { // 创建校验手机号过滤器实例 MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter(); // 接收 AuthenticationManager 认证管理器 mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // 采用哪个成功、失败处理器 mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler); mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler); //手机登录记住我功能 mobileAuthenticationFilter.setRememberMeServices(http.getSharedObject(RememberMeServices.class)); // 为 Provider 指定明确 的mobileUserDetailsService 来查询用户信息 MobileAuthenticationProvider provider = new MobileAuthenticationProvider(); provider.setUserDetailsService(mobileUserDetailsService); // 将 provider 绑定到 HttpSecurity 上面, // 并且将 手机认证加到 用户名密码认证之后 http.authenticationProvider(provider).addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
10、绑定到安全配置 SpringSecurityConfig
向 SpringSecurityConfig 中注入SmsVerifyCodeValidateFilter和MobileAuthenticationConfig实例
将 SmsVerifyCodeValidateFilter实例添加到UsernamePasswordAuthenticationFilter前面
http .addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
// 将手机相关的配置绑定过滤器链上 http.apply(mobileAuthenticationConfig);
11、SpringSecurityConfig安全配置类完整代码
/** * 安全配置类作为安全控制中心, 用于实现身份认证与授权配置功能 */ @Configuration @EnableWebSecurity //启动 SpringSecurity 过滤器链功能 public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired SecurityProperties securityProperties; Logger logger = LoggerFactory.getLogger(SpringSecurityConfig.class); @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { // 加密存储 明文+随机盐值 return new BCryptPasswordEncoder(); } @Autowired CustomUserDetailsService customUserDetailsService; /** * 认证管理器: * 1、认证信息提供方式(用户名、密码、当前用户的资源权限) * 2、可采用内存存储方式,也可能采用数据库方式等 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //基于内存存储认证信息 存储的密码必须是加密后的 不然会报错:There is no PasswordEncoder mapped for the id "null" //auth.inMemoryAuthentication().withUser("zcc").password("123").authorities("ADMIN"); /*String password = bCryptPasswordEncoder().encode("123"); logger.info("加密后的密码:" + password); auth.inMemoryAuthentication().withUser("zcc").password(password).authorities("ADMIN");*/ // 指定使用自定义查询用户信息来完成身份认证 auth.userDetailsService(customUserDetailsService); } @Autowired CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Autowired ImageVerifyCodeValidateFilter imageVerifyCodeValidateFilter; @Autowired SmsVerifyCodeValidateFilter smsVerifyCodeValidateFilter; @Autowired MobileAuthenticationConfig mobileAuthenticationConfig; /** * 记住我 功能 */ @Autowired DataSource dataSource; @Bean public JdbcTokenRepositoryImpl jdbcTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // 是否启动时自动创建表,第一次启动创建就行,后面启动把这个注释掉,不然报错已存在表 //jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } /** * 资源权限配置(过滤器链): * 1、被拦截的资源 * 2、资源所对应的角色权限 * 3、定义认证方式:httpBasic 、httpForm * 4、定制登录页面、登录请求地址、错误处理方式 * 5、自定义 spring security 过滤器 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //http.httpBasic()//采用httpBasic 认证方式 /*http.formLogin() .loginPage("/login/page")// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl("/login/form") // 登录表单提交处理Url, 默认是 /login .usernameParameter("name") // 默认用户名的属性名是 username .passwordParameter("pwd") // 默认密码的属性名是 password .and() .authorizeRequests()//认证请求 .antMatchers("/login/page").permitAll()//自定义登录页不需要认证 .anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证*/ http .addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .formLogin() .loginPage(securityProperties.getLoginPage())// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl(securityProperties.getLoginProcessingUrl()) // 登录表单提交处理Url, 默认是 /login .usernameParameter(securityProperties.getUsernameParameter()) // 默认用户名的属性名是 username .passwordParameter(securityProperties.getPasswordParameter()) // 默认密码的属性名是 password .successHandler(customAuthenticationSuccessHandler)//自定义认证成功处理器 .failureHandler(customAuthenticationFailureHandler)//自定义认证失败处理器 .and() .authorizeRequests()//认证请求 .antMatchers(securityProperties.getLoginPage(),securityProperties.getMobilePage(),securityProperties.getImageCodeUrl(),securityProperties.getMobileCodeUrl()).permitAll()//自定义登录页不需要认证,生成图片验证码,发送短信获取验证码也不需要验证 .anyRequest().authenticated()// 所有进入应用的HTTP请求都要进行认证 .and() .rememberMe()//记住我功能 .tokenRepository(jdbcTokenRepository())//保存登录信息 .tokenValiditySeconds(securityProperties.getTokenValiditySeconds());//记住我有效时长一周 // 将手机相关的配置绑定过滤器链上 http.apply(mobileAuthenticationConfig); } /** * 放行静态资源(js css 等) * * @param web */ @Override public void configure(WebSecurity web) { //web.ignoring().antMatchers("/dist/**", "/modules/**", "/plugins/**"); web.ignoring().antMatchers(securityProperties.getStaticPaths()); } }
12、测试
记住我功能测试:(记住我 的 input 标签的 name="remember-me")
完整代码地址:https://gitee.com/zhechaochao/security-parent.git