若依系统(Security)增加手机验证码登录

目录

本文参考链接:https://www.jianshu.com/p/54ac94a98390

一、编写token类

二、编写短信登陆鉴权 Provider

三、编写UserDetailsService实现类

四、编写SecurityConfig配置类


本文参考链接:https://www.jianshu.com/p/54ac94a98390

 感谢作者大大,写的很好。此篇就是简单记录一下。

一、编写token类

package com.demo.framework.security.sms;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/**
 * 短信登录 AuthenticationToken,模仿 UsernamePasswordAuthenticationToken 实现
 *
 * @author gmk
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        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();
    }

}

二、编写短信登陆鉴权 Provider

package com.demo.framework.security.sms;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 *
 * @author gmk
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String telephone = (String) authenticationToken.getPrincipal();

        // 委托 UserDetailsService 查找系统用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);

        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

三、编写UserDetailsService实现类

这里是拿原先的修改的

package com.demo.framework.web.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.ybz.safety.common.core.domain.entity.SysUser;
import com.ybz.safety.common.core.domain.model.LoginUser;
import com.ybz.safety.common.enums.UserStatus;
import com.ybz.safety.common.exception.ServiceException;
import com.ybz.safety.common.utils.MessageUtil;
import com.ybz.safety.common.utils.StringUtils;
import com.ybz.safety.system.service.ISysUserService;

/**
 * 用户验证处理
 *
 * @author ruoyi
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPasswordService passwordService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException(MessageUtil.message("user.not.exists"));
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException(MessageUtil.message("user.blocked"));
        }

//        passwordService.validate(user);

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

四、编写SecurityConfig配置类

package com.demo.framework.config;

import com.ybz.safety.framework.config.properties.PermitAllUrlProperties;
import com.ybz.safety.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ybz.safety.framework.security.handle.AuthenticationEntryPointImpl;
import com.ybz.safety.framework.security.handle.LogoutSuccessHandlerImpl;
import com.ybz.safety.framework.security.sms.SmsCodeAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;

/**
 * spring security配置
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 允许匿名访问的地址
     */
    @Autowired
    private PermitAllUrlProperties permitAllUrl;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        // 注解标记允许匿名访问的url
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable().and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/sendSms","/login", "/register", "/captchaImage").permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**",
                        "/images/**", "/**/pdf/**").permitAll()
                .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // 添加Logout filter
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class)
                //手机验证码的provider
                .authenticationProvider(smsCodeAuthenticationProvider);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //账号密码
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

四、登录接口

  @Autowired
    private ISysUserService userService;
    
    @Resource
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenService tokenService;

    @Autowired
    private RedisCache redisCache;
    
    @PostMapping(value = {"/login"})
    public AjaxResult login(@RequestBody Map<String, Object> params) {
        try {
            String phone = params.get("phone").toString();
            String smsCode = params.get("smsCode").toString();
            if (StringUtils.isEmpty(phone)) {
                return AjaxResult.error("手机号码不能为空");
            }

            if (phone.trim().length() != 11) {
                return AjaxResult.error("手机号码格式不正确");
            }
            SysUser sysUser = userService.selectUserByUserName(phone);
            if (sysUser == null) {
                return AjaxResult.error("登录帐号不正确");
            }
            if (!UserStatus.OK.getCode().equals(sysUser.getStatus())) {
                return AjaxResult.error("账号不存在或不可用");
            }
            
            //校验验证码(这一块代码就不写了)

            
           
            
            
            // 用户验证
            Authentication authentication = null;
            try {
                // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
                authentication = authenticationManager.authenticate(new SmsCodeAuthenticationToken(phone));
            } catch (Exception e) {
                if (e instanceof BadCredentialsException) {
                    AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, Constants.LOGIN_FAIL, MessageUtil.message("user.password.not.match")));
                    throw new UserPasswordNotMatchException();
                } else {
                    AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, Constants.LOGIN_FAIL, e.getMessage()));
                    throw new ServiceException(e.getMessage());
                }
            }
            // 执行异步任务,记录登录信息
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, Constants.LOGIN_SUCCESS, MessageUtil.message("user.login.success")));
            // 获取登录人信息
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            // 生成token
            String token = tokenService.createToken(loginUser);
            redisCache.setCacheObject("token", token);
            return AjaxResult.success(sysUser).put("token", token);
        } catch (Exception e) {
            this.log.error("异常:登录失败", e);
        }
        return AjaxResult.error("登录失败,请稍后再试");
    }

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Security可以很方便地实现手机验证码登录,步骤如下: 1. 添加spring-security-web和spring-security-config依赖。 ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring-security.version}</version> </dependency> ``` 2. 创建一个实现UserDetailsService接口的类,该类用于根据不同的用户名加载用户信息。 ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户名不存在"); } return new UserPrincipal(user); } } ``` 3. 创建一个实现AuthenticationProvider接口的类,该类用于验证用户的手机号和验证码是否正确。 ```java @Service public class SmsCodeAuthenticationProvider implements AuthenticationProvider { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String mobile = authenticationToken.getPrincipal().toString(); String code = authenticationToken.getCredentials().toString(); String redisCode = redisTemplate.opsForValue().get(SmsCodeAuthenticationFilter.REDIS_SMS_CODE_KEY_PREFIX + mobile); if (StringUtils.isBlank(redisCode)) { throw new BadCredentialsException("验证码不存在或已过期"); } if (!StringUtils.equals(code, redisCode)) { throw new BadCredentialsException("验证码不正确"); } UserDetails userDetails = new UserPrincipal(new User(mobile, "", Collections.emptyList())); return new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } } ``` 4. 创建一个实现AuthenticationFilter接口的类,该类用于处理短信验证码登录请求。 ```java public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; public static final String SPRING_SECURITY_FORM_CODE_KEY = "code"; public static final String REDIS_SMS_CODE_KEY_PREFIX = "SMS_CODE_"; private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/login/mobile", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("不支持的请求方式: " + request.getMethod()); } String mobile = obtainMobile(request); String code = obtainCode(request); if (StringUtils.isBlank(mobile)) { throw new UsernameNotFoundException("手机号不能为空"); } if (StringUtils.isBlank(code)) { throw new BadCredentialsException("验证码不能为空"); } SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, code); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected String obtainCode(HttpServletRequest request) { return request.getParameter(codeParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String mobileParameter) { this.mobileParameter = mobileParameter; } public void setCodeParameter(String codeParameter) { this.codeParameter = codeParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } public final String getCodeParameter() { return codeParameter; } } ``` 5. 在WebSecurityConfigurerAdapter的子类中进行配置。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login/mobile").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login").defaultSuccessURL("/home").permitAll() .and() .logout().logoutUrl("/logout").permitAll() .and() .addFilterBefore(smsCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(smsCodeAuthenticationProvider) .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SmsCodeAuthenticationFilter smsCodeAuthenticationFilter() { SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); filter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.write("{\"code\": 0, \"message\": \"登录成功\"}"); out.flush(); out.close(); }); filter.setAuthenticationFailureHandler((request, response, exception) -> { response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.write("{\"code\": 1, \"message\": \"" + exception.getMessage() + "\"}"); out.flush(); out.close(); }); return filter; } } ``` 6. 在前端页面中添加短信验证码登录的表单。 ```html <form action="/login/mobile" method="post"> <div> <label>手机号:</label> <input type="text" name="mobile" /> </div> <div> <label>验证码:</label> <input type="text" name="code" /> <button type="button" onclick="sendSmsCode()">发送验证码</button> </div> <div> <button type="submit">登录</button> </div> </form> ``` 7. 在后端控制器中添加发送短信验证码的接口。 ```java @RestController public class SmsCodeController { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping("/sms/code") public void sendSmsCode(String mobile) { String code = RandomStringUtils.randomNumeric(6); redisTemplate.opsForValue().set(SmsCodeAuthenticationFilter.REDIS_SMS_CODE_KEY_PREFIX + mobile, code, 5, TimeUnit.MINUTES); // 发送短信验证码 } } ``` 以上就是使用Spring Security实现手机验证码登录的全部步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值