Spring Security系列(23)- Security Oauth2短信验证码授权模式获取令牌案例

前言

在Oauth2平台中,自家平台使用使用密码模式获取令牌,然后携带令牌去访问资源服务器。

对于首页登录来说,Security Oauth2已经提供了密码模式,但是肯定也需要其他模式,比如手机短信、QQ、微信等社交平台进行登录。

密码模式分析

在之前Oauth2端点源码解析中,我们分析了端点令牌发放的源码,了解到是有令牌颁发器来发放的。
Spring Security系列(23)- Security Oauth2短信验证码授权模式获取令牌案例_第1张图片
密码模式使用了ResourceOwnerPasswordTokenGranter,主要进行了以下几步操作:

  1. 获取请求中的用户名 密码
  2. 常见预认证对象UsernamePasswordAuthenticationToken
  3. 调用认证管理器,调用ProviderManager进行认证
  4. 认证成功,返回OAuth2Authentication认证对象
    Spring Security系列(23)- Security Oauth2短信验证码授权模式获取令牌案例_第2张图片

总结: 我们自定义一个短信登录的令牌颁发器就行了,直接可以在其中进行认证,也可以交给认证管理器,调用认证程序进行认证。

手机短信登录功能实现案例

1. 添加获取短信验证码接口

@RestController
@RequestMapping("/sms")
@Slf4j
public class SmsEndpoint {
     

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 发送验证码接口
     *
     * @param phone
     * @return
     */
    @GetMapping("/send/code")
    public Map<String,String> msmCode(String phone) {
     
        // 1. 获取到手机号
        log.info(phone + "请求获取验证码");
        // 2. 模拟调用短信平台获取验证码,以手机号为KEY,验证码为值,存入Redis,过期时间一分钟
        String code = RandomUtil.randomNumbers(6);
        redisTemplate.opsForValue().setIfAbsent(phone, code, 60*10, TimeUnit.SECONDS);
        String saveCode = redisTemplate.opsForValue().get(phone);// 缓存中的code
        Long expire = redisTemplate.opsForValue().getOperations().getExpire(phone, TimeUnit.SECONDS); // 查询过期时间
        // 3. 验证码应该通过短信发给用户,这里直接返回吧
        Map<String,String> result=new HashMap<>();
        result.put("code",saveCode);
        result.put("过期时间",expire+"秒");
        return result;
    }
}

2. 创建短信登录认证令牌

public class SmsAuthenticationToken extends AbstractAuthenticationToken {
     

    private static final long serialVersionUID = 1L;

    private final Object principal;
    private Object credentials;

    public SmsAuthenticationToken(Object principal, Object credentials) {
     
        super((Collection) null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
     
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
     
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
     
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
     
        Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
     
        super.eraseCredentials();
        this.credentials = null;
    }
}

3. 创建短信短信登录Provider

public class SmsAuthenticationProvider implements AuthenticationProvider {
     

    private UserDetailsService userDetailsServiceImpl;

    private RedisTemplate<String, String> redisTemplate;

    public SmsAuthenticationProvider(UserDetailsService userDetailsServiceImpl, RedisTemplate<String, String> redisTemplate) {
     
        this.userDetailsServiceImpl = userDetailsServiceImpl;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
     
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
        String phone = "";
        if (principal instanceof UserDetails) {
     
            phone = ((UserDetails)principal).getUsername();
        } else if (principal instanceof AuthenticatedPrincipal) {
     
            phone = ((AuthenticatedPrincipal)principal).getName();
        } else if (principal instanceof Principal) {
     
            phone = ((Principal)principal).getName();
        } else {
     
            phone = principal == null ? "" : principal.toString();
        }
        String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
        // 1. 检验Redis手机号的验证码
        String redisCode = redisTemplate.opsForValue().get(phone);
        if (StrUtil.isEmpty(redisCode)) {
     
            throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
        }
        if (!inputCode.equals(redisCode)) {
     
            throw new BadCredentialsException("输入的验证码不正确,请重新输入");
        }
        // 2. 根据手机号查询用户信息, 这里演示,直接查了user的信息
        UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername("user");
        if (userDetails == null) {
     
            throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
        }
        // 3. 重新创建已认证对象,
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(principal,inputCode, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> aClass) {
     
        return SmsAuthenticationToken.class.isAssignableFrom(aClass);
    }


}

4. 创建短信验证码登录令牌颁发器

public class SmsCodeGranter extends AbstractTokenGranter {
     
	// 修改授权模式为sms_code
    private static final String GRANT_TYPE = "sms_code";

    private final AuthenticationManager authenticationManager;

    public SmsCodeGranter(AuthenticationManager authenticationManager,
                          AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
     
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected SmsCodeGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
                             ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
     
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
     

        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        String phone = parameters.get("phone");
        String smsCode = parameters.get("smsCode");
        // Protect from downstream leaks of password
        parameters.remove("smsCode");
        Authentication userAuth = new SmsAuthenticationToken(phone, smsCode);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
     
            userAuth = authenticationManager.authenticate(userAuth);
            if (userAuth == null) {
     
                throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
            }
        } catch (AccountStatusException ase) {
     
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        } catch (BadCredentialsException e) {
     
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
     
            throw new InvalidGrantException("Could not authenticate user: " + phone);
        }
        // 3. 重新创建Oau已认证对象,
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);

    }
}

5. 修改授权服务器配置

修改WebSecurityConfigure,主要是在认证管理器中添加SmsAuthenticationProvider。

@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
     

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    public void configure(WebSecurity web) throws Exception {
     
        // 将 check_token 暴露出去,否则资源服务器访问时报错
        web.ignoring().antMatchers("/oauth/check_token","/sms/send/code");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        super.configure(http);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     
        auth.inMemoryAuthentication()
                // 在内存中创建用户并为密码加密
                .withUser("user").password(passwordEncoder().encode("123456")).roles("USER");
    }

    // 密码解析器
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
     
        return new BCryptPasswordEncoder();
    }


    // 配置认证管理器
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
     
        return super.authenticationManagerBean();
    }


    /**
     * 将Provider添加到认证管理器中
     *
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
     
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(new SmsAuthenticationProvider(myUserDetailsService,redisTemplate), daoAuthenticationProvider()));
        authenticationManager.setEraseCredentialsAfterAuthentication(false);
        return authenticationManager;
    }


    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
     
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false); // 设置显示找不到用户异常
        return daoAuthenticationProvider;
    }

}

AuthorizationServerConfigure配置类中,主要是将颁发器添加到端点配置中。

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
     

    // AuthorizationServer配置
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
     
        security
                // tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开
                .tokenKeyAccess("permitAll()")
                // checkToken这个endpoint完全公开
                .checkTokenAccess("permitAll()")
                //  允许表单认证
                .allowFormAuthenticationForClients();
    }

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenStore jwtTokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    MyWebResponseExceptionTranslator myWebResponseExceptionTranslator;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     
        // 配置客户端
        clients
                // 使用内存设置
                .inMemory()
                // client_id
                .withClient("client")
                // client_secret
                .secret(passwordEncoder.encode("secret"))
                // 授权类型: 授权码、刷新令牌、密码、客户端、简化模式、短信验证码
                .authorizedGrantTypes("authorization_code", "refresh_token", "password", "client_credentials", "implicit", "sms_code")
                // 授权范围,也可根据这个范围标识,进行鉴权
                .scopes("app")
                // 授权码模式 授权页面是否自动授权
                //.autoApprove(false)
                // 拥有的权限
                .authorities("add:user")
                // 允许访问的资源服务 ID
                //.resourceIds("oauth2-resource-server001-demo")
                // 注册回调地址
                .redirectUris("http://localhost:20000/code", "http://localhost:9001/resource001/login");
    }

    // 端点配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
     
        // 配置端点允许的请求方式
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        // 配置认证管理器
        endpoints.authenticationManager(authenticationManager);
        // 自定义异常翻译器,用于处理OAuth2Exception
        endpoints.exceptionTranslator(myWebResponseExceptionTranslator);
        // 重新组装令牌颁发者,加入自定义授权模式
        endpoints.tokenGranter(getTokenGranter(endpoints));
/*      // 添加JWT令牌
        // JWT令牌转换器
        endpoints.accessTokenConverter(jwtAccessTokenConverter);
        // JWT 存储令牌
        endpoints.tokenStore(jwtTokenStore);*/
    }

    private TokenGranter getTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
     
        // 默认tokenGranter集合
        List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
        // 增加短信验证码模式
        granters.add(new SmsCodeGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
        // 组合tokenGranter集合
        return new CompositeTokenGranter(granters);
    }
}

测试

1. 获取验证码

Spring Security系列(23)- Security Oauth2短信验证码授权模式获取令牌案例_第3张图片

2. 使用验证码授权模式登陆

Spring Security系列(23)- Security Oauth2短信验证码授权模式获取令牌案例_第4张图片

3. 使用用户名密码模式

Spring Security系列(23)- Security Oauth2短信验证码授权模式获取令牌案例_第5张图片

你可能感兴趣的:(spring,java,security)