使用springSecurity及OAuth2实现多模式登录

使用springSecurity及OAuth2实现多模式登录

  • 账号密码模式登录
    • 初始化相关配置类
      • security相关配置
      • OAuth2相关配置
      • JWT生成token相关配置
      • token参数增强设置
    • granter设置
      • 账号密码登录granter
    • UserDetailsService
    • 用户实体类
    • main方法启动类
    • 测试验证
  • 短信验证码模式登录
    • 初始化相关配置类修改
      • AuthorizationServerConfig添加短信验证码登录模式
      • WhqSecurityConfig添加短信验证码provider配置
    • granter设置
      • 短信验证码模式granter
    • 短信验证码provider
    • 短信验证码UserDetailsService
    • 短信验证码AuthenticationToken
    • 测试验证
  • 总结

此篇文章只会给出通过springSecurity和springOAuth2实现多模式登录的示例,另外一篇文章会在此示例的基础上进行源码分析。文章最后会给出源码分析链接地址。
以下所有的代码已经上传至github:
https://github.com/huanqinglord/security-demo

账号密码模式登录

首先只实现账号密码的登录模式。

初始化相关配置类

security相关配置

package com.whq.security.oauth.config;

import com.whq.security.oauth.provider.SMSAuthenticationProvider;
import com.whq.security.oauth.service.MyUserDetailsService;
import com.whq.security.oauth.service.SMSUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * spring security 相关配置
 */
@Configuration
public class WhqSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.配置基本认证方式
        http.authorizeRequests()
                // 角色为“ADMIN”的用户才可以访问/test/admin/相关的接口
                //.antMatchers("/test/admin/**").hasRole("ADMIN")
                // 角色为"USER"、“ADMIN”的用户才可以访问/test/user/相关的接口
                //.antMatchers("/test/user/**").hasAnyRole("USER", "ADMIN")
                // 所有用户都可以访问的接口
                .antMatchers("/swagger/**", "/oauth/token/**").permitAll()
                // 对任意请求都进行认证(其他路径的请求登录后才可以访问)
                .anyRequest()
                .authenticated()
                //开启basic认证
                .and().httpBasic()
                .and().formLogin();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 初始化security的认证管理器
    @Bean("authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 账号密码登录设置获取用户UserDetailsService
    // 如果所有登录方式获取用户的方式一致,则可以在AuthorizationServerConfigurerAdapter的AuthorizationServerEndpointsConfigurer中设置UserDetailsService
    @Bean
    public DaoAuthenticationProvider getDaoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());

        return daoAuthenticationProvider;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(getDaoAuthenticationProvider());
    }
}

OAuth2相关配置

package com.whq.security.oauth.config;

import com.whq.security.oauth.granter.WhqTokenGranter;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.ArrayList;
import java.util.List;

/**
 * spring security oauth2 相关配置
 */
@Configuration
@AllArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;
    private final PasswordEncoder passwordEncoder;
    private final TokenStore tokenStore;
    private final TokenEnhancer jwtTokenEnhancer;
    private final JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        //获取自定义tokenGranter
        TokenGranter tokenGranter = WhqTokenGranter.getTokenGranter(authenticationManager, endpoints);
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .tokenGranter(tokenGranter);
        // 扩展token返回结果
        if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            List enhancerList = new ArrayList<>();
            enhancerList.add(jwtTokenEnhancer);
            enhancerList.add(jwtAccessTokenConverter);
            tokenEnhancerChain.setTokenEnhancers(enhancerList);
            // jwt增强
            endpoints.tokenEnhancer(tokenEnhancerChain).accessTokenConverter(jwtAccessTokenConverter);
        }
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 将client-secret存储在内存中
        // 如果需要从数据库加载则继承JdbcClientDetailsService类自定义处理
        // clients.withClientDetails();

        // basic验证用户名:whqClient 密码:whqSecret
        clients.inMemory().withClient("whqClient").secret(passwordEncoder.encode("whqSecret"))
                // 令牌有效时间,单位秒
                .accessTokenValiditySeconds(7200)
                // 支持账号密码模式登录
                .authorizedGrantTypes("refresh_token",  "pwd")
                // 权限有哪些,如果这两配置了该参数,客户端发请求可以不带参数,使用配置的参数
                .scopes("all", "read", "write");
    }
}

JWT生成token相关配置

package com.whq.security.oauth.config;


import com.whq.security.oauth.factory.JwtTokenEnhancer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * jwt 生成token相关配置
 */
@Configuration
public class JwtTokenStoreConfiguration {

	/**
	 * 使用jwtTokenStore存储token
	 */
	@Bean
	public TokenStore jwtTokenStore() {
		return new JwtTokenStore(jwtAccessTokenConverter());
	}

	/**
	 * 用于生成jwt
	 */
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
		accessTokenConverter.setSigningKey("whq");
		return accessTokenConverter;
	}

	/**
	 * 用于扩展jwt
	 */
	@Bean
	@ConditionalOnMissingBean(name = "jwtTokenEnhancer")
	public TokenEnhancer jwtTokenEnhancer(JwtAccessTokenConverter jwtAccessTokenConverter) {
		return new JwtTokenEnhancer(jwtAccessTokenConverter);
	}

}

token参数增强设置


package com.whq.security.oauth.factory;


import com.whq.security.oauth.user.MyUser;
import lombok.AllArgsConstructor;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.HashMap;
import java.util.Map;

@AllArgsConstructor
public class JwtTokenEnhancer implements TokenEnhancer {

	private final JwtAccessTokenConverter jwtAccessTokenConverter;

	@Override
	public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
		MyUser principal = (MyUser) authentication.getUserAuthentication().getPrincipal();

		// token参数增强
		// todo 以下可根据自身业务自由设置,方便于解析token后获取到有价值的信息
		Map info = new HashMap<>(16);
		info.put("user_id", principal.getUserId());
		info.put("user_name", principal.getUsername());
		info.put("user_phone", "18788888888");
		((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);

		OAuth2AccessToken oAuth2AccessToken = jwtAccessTokenConverter.enhance(accessToken, authentication);
		String tokenValue = oAuth2AccessToken.getValue();
		String tenantId = principal.getTenantId();
		String userId = principal.getUserId() == null ? "" : principal.getUserId().toString();
		// todo 此处可以将token存入redis
		// RedisTokenUtil.addAccessToken();

		return accessToken;
	}
}

granter设置


package com.whq.security.oauth.granter;


import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 自定义拓展TokenGranter
 */
public class WhqTokenGranter {

	/**
	 * 自定义tokenGranter
	 */
	public static TokenGranter getTokenGranter(final AuthenticationManager authenticationManager, final AuthorizationServerEndpointsConfigurer endpoints) {
		// 默认tokenGranter集合
		List granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
		// 账号密码模式
		granters.add(new PasswordGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
		// 组合tokenGranter集合
		return new CompositeTokenGranter(granters);
	}

}

账号密码登录granter

package com.whq.security.oauth.granter;

import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

import java.util.LinkedHashMap;
import java.util.Map;

public class PasswordGranter extends AbstractTokenGranter {

	public static final String GRANT_TYPE = "pwd";

	private final AuthenticationManager authenticationManager;

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

	protected PasswordGranter(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 parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
		String username = parameters.get("username");
		String cryptPassword = parameters.get("password");

		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, cryptPassword);
		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
		try {
			// 以下代码为spring security 授权认证逻辑
			userAuth = authenticationManager.authenticate(userAuth);
		} catch (AccountStatusException | BadCredentialsException ase) {
			throw new InvalidGrantException(ase.getMessage());
		}

		if (userAuth == null || !userAuth.isAuthenticated()) {
			throw new InvalidGrantException("Could not authenticate user: " + username);
		}

		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
		return new OAuth2Authentication(storedOAuth2Request, userAuth);
	}
}

UserDetailsService

此处设置加载用户信息的方式,账号密码使用登录账号获取用户信息。

package com.whq.security.oauth.service;

import com.whq.security.oauth.user.MyUser;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    // 后续登录使用此方法加载用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser result = MyUser.getUser(username);
        // 模拟的用户查询数据为明文密码,实际使用时都是加密存储,此处手动加密模拟处理
        String encode = passwordEncoder.encode(result.getPassword());
        // 用户不存在,抛出异常
        if (result == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        MyUser res = new MyUser(result.getUsername(), encode);
        return res;
    }
}

用户实体类

此处实体类继承自org.springframework.security.core.userdetails.User

package com.whq.security.oauth.user;

import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * 用户信息实体类,实际项目中根据自身项目需求修改
 */
@Getter
@Setter
public class MyUser extends User {
    /**
     * 用户id
     */
    private final Long userId;
    /**
     * 租户ID
     */
    private final String tenantId;


    // 用户构造函数
    public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities, Long userId, String tenantId) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        this.userId = userId;
        this.tenantId = tenantId;
    }

    /**
     * 方便测试用的构造函数
     * 除用户名及密码外其他信息一致
     */
    public MyUser(String username, String password) {
        // authorities 参数为用户角色相关,如果没有使用spring security进行角色权限控制,则不需要配置此参数
        this(username, password, true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("USER,AMDIN"), 1L, "000000");
    }

    // 模拟数据库存储的用户信息
    public static Map myUsers;

    static {
        myUsers = new HashMap<>();
        myUsers.put("whq1", new MyUser("whq1", "123"));
        myUsers.put("whq2", new MyUser("whq2", "123"));
        myUsers.put("18788888888", new MyUser("whq1", "123"));
        myUsers.put("18666666666", new MyUser("whq2", "123"));
    }

    // 模拟数据库通过用户名查询用户信息
    public static MyUser getUser(String userName) {
        return myUsers.get(userName);
    }

    // 模拟数据库通过手机号查询用户信息
    public static MyUser getUserByPhone(String userName) {
        return myUsers.get(userName);
    }
}

main方法启动类

package com.whq.security.oauth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.whq")
public class SecurityOAuth2Application {
    public static void main(String[] args) {
        SpringApplication.run(SecurityOAuth2Application.class, args);
    }
}

测试验证

以上所有代码就实现了账号密码模式的登录。下来通过postman进行验证:

使用springSecurity及OAuth2实现多模式登录_第1张图片

短信验证码模式登录

初始化相关配置类修改

AuthorizationServerConfig添加短信验证码登录模式

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 将client-secret存储在内存中
        // 如果需要从数据库加载则继承JdbcClientDetailsService类自定义处理
        // clients.withClientDetails();

        // basic验证用户名:whqClient 密码:whqSecret
        clients.inMemory().withClient("whqClient").secret(passwordEncoder.encode("whqSecret"))
                // 令牌有效时间,单位秒
                .accessTokenValiditySeconds(7200)
                // 支持短信验证码、账号密码模式登录
                .authorizedGrantTypes("refresh_token", "SMSVerification", "pwd")
                // 权限有哪些,如果这两配置了该参数,客户端发请求可以不带参数,使用配置的参数
                .scopes("all", "read", "write");
    }

WhqSecurityConfig添加短信验证码provider配置

package com.whq.security.oauth.config;

import com.whq.security.oauth.provider.SMSAuthenticationProvider;
import com.whq.security.oauth.service.MyUserDetailsService;
import com.whq.security.oauth.service.SMSUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * spring security 相关配置
 */
@Configuration
public class WhqSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    // 短信验证时获取用户方式改变
    @Autowired
    private SMSUserDetailsService smsUserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.配置基本认证方式
        http.authorizeRequests()
                // 角色为“ADMIN”的用户才可以访问/test/admin/相关的接口
                //.antMatchers("/test/admin/**").hasRole("ADMIN")
                // 角色为"USER"、“ADMIN”的用户才可以访问/test/user/相关的接口
                //.antMatchers("/test/user/**").hasAnyRole("USER", "ADMIN")
                // 所有用户都可以访问的接口
                .antMatchers("/swagger/**", "/oauth/token/**").permitAll()
                // 对任意请求都进行认证(其他路径的请求登录后才可以访问)
                .anyRequest()
                .authenticated()
                //开启basic认证
                .and().httpBasic()
                .and().formLogin();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 初始化security的认证管理器
    @Bean("authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 账号密码登录设置获取用户UserDetailsService
    // 如果所有登录方式获取用户的方式一致,则可以在AuthorizationServerConfigurerAdapter的AuthorizationServerEndpointsConfigurer中设置UserDetailsService
    @Bean
    public DaoAuthenticationProvider getDaoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());

        return daoAuthenticationProvider;
    }

    // 短信验证provider初始化
    @Bean
    public SMSAuthenticationProvider getSMSAuthenticationProvider(){
        SMSAuthenticationProvider smsAuthenticationProvider = new SMSAuthenticationProvider();
        smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService);

        return smsAuthenticationProvider;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(getDaoAuthenticationProvider());
        // 添加短信验证provider
        auth.authenticationProvider(getSMSAuthenticationProvider());
    }
}

granter设置

短信验证码模式granter

package com.whq.security.oauth.granter;


import com.whq.security.oauth.token.SMSVerificationAuthenticationToken;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

import java.util.LinkedHashMap;
import java.util.Map;

public class SMSVerificationTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "SMSVerification";

    private final AuthenticationManager authenticationManager;

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

    protected SMSVerificationTokenGranter(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 parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String smsCode = parameters.get("SMS-Code");
        String phone = parameters.get("phone");
        parameters.remove("SMS-Code");

        Authentication userAuth = new SMSVerificationAuthenticationToken(phone, smsCode);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            // 以下代码为spring security 授权认证逻辑
            userAuth = authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException | BadCredentialsException ase) {
            throw new InvalidGrantException(ase.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate user: " + phone);
        }

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

WhqTokenGranter类中对SMSVerificationTokenGranter进行初始化

public static TokenGranter getTokenGranter(final AuthenticationManager authenticationManager, final AuthorizationServerEndpointsConfigurer endpoints) {
		// 默认tokenGranter集合
		List granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
		// 账号密码模式
		granters.add(new PasswordGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
		// 短信验证码模式
		granters.add(new SMSVerificationTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
		// 组合tokenGranter集合
		return new CompositeTokenGranter(granters);
	}

短信验证码provider

package com.whq.security.oauth.provider;


import com.whq.security.oauth.token.SMSVerificationAuthenticationToken;
import com.whq.security.oauth.user.MyUser;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;

import java.util.ArrayList;

public class SMSAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

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

    /**
     * 短信认证
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取手机号
        String phone = (authentication.getPrincipal() == null ) ? null : authentication.getPrincipal().toString();
        // 获取短信验证码
        String smsCode = (authentication.getCredentials() == null ) ? null : authentication.getCredentials().toString();
        // todo 在此处进行短信验证码的校验
        // 此处模拟短信验证码为123
        if (!"123".equals(smsCode)) {
            throw new UserDeniedAuthorizationException("验证码不正确");
        }
        MyUser userDetails = (MyUser) userDetailsService.loadUserByUsername(phone);
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("当前手机号不存在,请先注册");
        }
        SMSVerificationAuthenticationToken smsVerificationAuthenticationToken = new SMSVerificationAuthenticationToken(new ArrayList(), userDetails, authentication.getCredentials());
        smsVerificationAuthenticationToken.setDetails(authentication.getDetails());
        return smsVerificationAuthenticationToken;
    }

    // 判断是否支持此登录方式
    @Override
    public boolean supports(Class authentication) {
        return (SMSVerificationAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

短信验证码UserDetailsService

此处如果账号密码获取用户信息的方式与短信验证码相同(比如:账号密码登录时也是通过手机号登录)则无需再添加此类,同时WhqSecurityConfig类中的配置只需要给SMSAuthenticationProvider设置相同的UserDetailsService即可。

package com.whq.security.oauth.service;

import com.whq.security.oauth.user.MyUser;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class SMSUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    // 后续登录使用此方法加载用户信息
    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        // 用户使用手机号登录
        MyUser result = MyUser.getUserByPhone(phone);
        // 模拟的用户查询数据为明文密码,实际使用时都是加密存储,此处手动加密模拟处理
        String encode = passwordEncoder.encode(result.getPassword());
        // 用户不存在,抛出异常
        if (result == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        MyUser res = new MyUser(result.getUsername(), encode);
        return res;
    }
}

短信验证码AuthenticationToken

package com.whq.security.oauth.token;

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

import java.util.Collection;

public class SMSVerificationAuthenticationToken extends AbstractAuthenticationToken {

    // 存储登录手机号
    private Object principal;

    // 存储登录短信验证码
    private Object credentials;

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

    /**
     * 校验通过时使用此构造函数
     * 设置参数super.setAuthenticated(true); 表示短信验证码登录校验通过,准备生成token
     */
    public SMSVerificationAuthenticationToken(Collection authorities, Object principal, Object credentials) {
        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;
    }
}

测试验证

使用springSecurity及OAuth2实现多模式登录_第2张图片

总结

springOAuth2添加新的认证模式需要完成如下工作:

  1. 配置文件中添加新的authorizedGrantTypes(比如示例中的SMSVerification)
  2. 添加新模式的granter
  3. 添加具体的provider进行认证
  4. 添加新模式对应的AuthenticationToken
  5. 如果新模式获取用户信息的方式是特立独行的,还需要实现新的UserDetailsService,同时需要将新的UserDetailsService与provider进行关联(示例中在WhqSecurityConfig配置类中进行处理的)

基于上述示例,通过DEBUG对springSecurity和springOAuth2进行源码分析,文章链接地址如下:
springSecurity及springOAuth2源码解析

文章内容结合实际工作经验编写,如有问题,欢迎大家评论指正。

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