Spring Cloud 集成 Spring Security和Oauth2.0实现认证授权(一)自定义授权模式

220 阅读2分钟

自定义授权模式(手机号+验证码)

oauth2提供的四种授权模式分别是授权码模式、密码模式、客户端凭证模式和隐式授权模式。Spring Security的实现主要是通过四个不同的TokenGranter来实现的,通过源码的中的抽象类AbstractTokenGranter查看继承它的子类。

image.png 通过子类的实现可以清晰地看到四种不同的授权模式实现类。接下来我们自定义授权模式(手机号+验证码)。

自定义Token

通过继承AbstractAuthenticationToken抽象类来实现我们需要记录的信息,包括手机号、密码、验证码和储存验证码的key。

public class MobilePasswordAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // mobile phone
    private final Object principal;

    // password
    private Object credentials;

    // verify code
    private final String code;

    private final String key;

    // before authentication , authorities is null
    public MobilePasswordAuthenticationToken(String mobilePhone, String password, String code, String key) {
        super(null);
        this.principal = mobilePhone;
        this.credentials = password;
        this.code = code;
        this.key = key;
        super.setAuthenticated(false);
    }
    // after authentication , get the authorities
    public MobilePasswordAuthenticationToken(Object principal, Object credentials, String code, Collection<? extends GrantedAuthority> authorities, String key) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        this.code = code;
        this.key = key;
        super.setAuthenticated(true);
    }

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

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

    public String getCode() {
        return code;
    }

    public String getKey() {
        return key;
    }

    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);
        }
    }

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

自定义TokenGranter

通过自定义的TokenGranter对请求头的信息进行封装,生成我们自定义的MobilePasswordAuthenticationToken,交给实际处理认证的Provider进行认证。

public class MobilePassWordGranter extends AbstractTokenGranter {
    // 自定义授权模式的标识符,用来判断请求头使用哪种授权模式
    private static final String GRANT_TYPE = "mobile_code";

    private final AuthenticationManager authenticationManager;

    public MobilePassWordGranter(AuthorizationServerTokenServices tokenServices,
                                    ClientDetailsService clientDetailsService,
                                    OAuth2RequestFactory requestFactory,
                                 AuthenticationManager authenticationManager
                                    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    /**
     * 封装MobilePasswordAuthenticationToken,进行认证
     * @param client ClientDetails
     * @param tokenRequest TokenRequest
     * @return OAuth2Authentication
     */
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        // 从请求头中取出携带的参数,参数名可以自定义
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        String username = parameters.get("username");
        String password = parameters.get("password");
        String code = parameters.get("code");
        String key = parameters.get("key");

        log.info("username=={}, password=={}, code=={}, key=={}", username, password, code, key);

        // Protect from downstream leaks of password这里可以将请求头中的密码去掉,保护请求信息
        parameters.remove("password");
        // 生成我们自定义的Token,用于下一步的认证
        Authentication userAuth = new MobilePasswordAuthenticationToken(username, password, code, key);

        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        userAuth = authenticationManager.authenticate(userAuth);

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);

        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

自定义认证处理器AuthenticationProvider

通过继承AuthenticationProvider来自定义我们的authenticate方法,实现认证逻辑。

public class MobilePasswordAuthenticationProvider implements AuthenticationProvider {

    private final PasswordEncoder passwordEncoder;

    private final SmsCodeUserDetailService smsCodeUserDetailService;

    private final RedisCache redisCache;

    private final RSAUtils rsaUtils;


    public MobilePasswordAuthenticationProvider(SmsCodeUserDetailService smsCodeUserDetailService, PasswordEncoder passwordEncoder, RedisCache redisCache, RSAUtils rsaUtils) {
        this.smsCodeUserDetailService = smsCodeUserDetailService;
        this.redisCache = redisCache;
        this.rsaUtils = rsaUtils;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        MobilePasswordAuthenticationToken authenticationToken = (MobilePasswordAuthenticationToken) authentication;
        // 取出我们封装的参数信息
        String mobile = authenticationToken.getPrincipal().toString();

        String password = authenticationToken.getCredentials().toString();

        String code = authenticationToken.getCode();

        String key = authenticationToken.getKey();
        // 参数校验
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password))
            throw new ServerOauth2Exception("账号密码不能为空!!!");

        if (StringUtils.isEmpty(code) || StringUtils.isEmpty(key))
            throw new ServerOauth2Exception("验证码不能为空!!!");

        String codeCache = redisCache.getObject(key);

        if (StringUtils.isEmpty(codeCache))
            throw new ServerOauth2Exception("验证码已过期!!!");

        if (!StringUtils.equals(code, codeCache))
            throw new ServerOauth2Exception("验证码错误!!!");
        // 从数据库中加载对应手机号的用户
        LoginUser loginUser = smsCodeUserDetailService.loadUserByPhone(mobile);

        if (ObjectUtil.isNull(loginUser))
            throw new ServerOauth2Exception("账号不存在!!!");
            // 这里是使用非对称加密来传输前端密码,可忽略
//        try {
//            // 私钥解密公钥加密的密码
//            password = rsaUtils.decryptByPrivateKey(password);
//        } catch (Exception e){
//            throw new ServerOauth2Exception(e.getMessage());
//        }
        // 通过passwordEncoder来匹配数据库中加密的用户密码和请求中传输的密码是否匹配
        if (!passwordEncoder.matches(password, loginUser.getPassword()))
            throw new ServerOauth2Exception("密码不正确!!!");
        // 将完整的用户信息封装到自定义Token中,包括用户权限信息
        MobilePasswordAuthenticationToken mobilePasswordAuthenticationToken = new MobilePasswordAuthenticationToken(loginUser, password, null, loginUser.getAuthorities(), "");

        mobilePasswordAuthenticationToken.setDetails(authentication.getDetails());

        return mobilePasswordAuthenticationToken;
    }

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

从数据库中加载用户信息

这里就是一个普通的service接口和实现类,无需继承其他类,通过依赖注入使用即可。

public class SmsCodeUserDetailServiceImpl implements SmsCodeUserDetailService {

    private SysAuthMapper sysAuthMapper;
    @Autowired
    public void setSysAuthMapper(SysAuthMapper sysAuthMapper) {
        this.sysAuthMapper = sysAuthMapper;
    }

    public LoginUser loadUserByPhone(String phone) throws UsernameNotFoundException {

        SysUser sysUserByPhone = sysAuthMapper.findSysUserByPhone(phone);

        if (Objects.isNull(sysUserByPhone))
            throw new ServerOauth2Exception("用户不存在!!!");

        LoginUser loginUser = new LoginUser()
                .setUserId(sysUserByPhone.getUserId())
                .setPhone(sysUserByPhone.getPhoneNumber())
                .setPassword(sysUserByPhone.getPassword());

        List<SysPermission> permissions = sysUserByPhone.getSysRole().getPermissions();
        List<SimpleGrantedAuthority> authorities = permissions.stream()
                .map(sysPermission -> new SimpleGrantedAuthority(sysPermission.getPermissionName())).collect(Collectors.toList());
        loginUser.setAuthorities(authorities);
        return loginUser;
    }
}