从实战的角度谈微服务(九):对于Spring Cloud Security的使用自定义授权类型

一、简介

Spring Cloud OAuth2.0默认支持的4种授权类型,分别如下:

  • 授权码模式
  • 简化模式
  • 客户端模式
  • 密码模式

实际生产中上述四种授权类型无法支撑业务场景,需进一步扩展授权类型,比如微信认证、手机号+密码、手机号+验证码认证、图形验证码认证、邮箱认证等。
下面将根据手机号+密码方式授权类型扩展,其他类型可类比实现。

二 步骤说明

具体步骤如下:

2.1 自定义UserDetailService

package com.chl.auth.service;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 *
 */
public interface IUserDetailsService extends UserDetailsService {
    /**
     * 判断实现类是否属于该类型
     *
     * @param accountType 账号类型
     */
    boolean supports(String accountType);

    /**
     * 根据电话号码查询用户
     *
     * @param mobile
     * @return
     */
    UserDetails loadUserByMobile(String mobile);
}
package com.chl.auth.service.impl;

import com.chl.auth.constant.SecurityConstants;
import com.chl.auth.service.IUserDetailsService;
import org.springframework.security.core.userdetails.User;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 用户查询接口
 *
 * @author chlin
 */
@Service
public class MyUserDetailsServiceImpl implements IUserDetailsService {

    private static final String ACCOUNT_TYPE = SecurityConstants.DEF_ACCOUNT_TYPE;

    @Override
    public boolean supports(String accountType) {
        return ACCOUNT_TYPE.equals(accountType);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //角色需要加前缀ROLE_  用于sec:authorize="hasRole('vip1')",默认匹配ROLE_vip1
        if ("root".equals(username)) {
            return User.withUsername("root").password(new BCryptPasswordEncoder().encode("123456")).authorities("r1", "r2", "r3").build();
        } else if ("guest".equals(username)) {
            return User.withUsername("guest").password(new BCryptPasswordEncoder().encode("123456")).authorities("r2").build();
        }
        return null;
    }

    @Override
    public UserDetails loadUserByMobile(String mobile) {
        //角色需要加前缀ROLE_  用于sec:authorize="hasRole('vip1')",默认匹配ROLE_vip1
        if ("18888888888".equals(mobile)) {
            return User.withUsername("18888888888").password(new BCryptPasswordEncoder().encode("123456")).authorities("r1", "r2", "r3").build();
        }
        return null;
    }
}

2.2 自定义AuthenticationToken

/**
 * 
 */
package com.chl.auth.security.token;

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

import java.util.Collection;

/**
 */
public class MobileAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================

	private final Object principal;
	private Object credentials;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * This constructor can be safely used by any code that wishes to create a
	 * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
	 * will return <code>false</code>.
	 *
	 */
	public MobileAuthenticationToken(String mobile, String password) {
		super(null);
		this.principal = mobile;
		this.credentials = password;
		setAuthenticated(false);
	}

	/**
	 * This constructor should only be used by <code>AuthenticationManager</code> or
	 * <code>AuthenticationProvider</code> implementations that are satisfied with
	 * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
	 * authentication token.
	 *
	 * @param principal
	 * @param authorities
	 */
	public MobileAuthenticationToken(Object principal, Object credentials,
                                     Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true);
	}

	// ~ Methods
	// ========================================================================================================

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

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

	@Override
	public void setAuthenticated(boolean isAuthenticated) {
		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();
	}
}

2.3 自定义TokenGranter

package com.chl.auth.security.granter;

import com.chl.auth.security.token.MobileAuthenticationToken;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
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 MobilePwdGranter extends AbstractTokenGranter {
    private static final String GRANT_TYPE = "mobile_password";

    private final AuthenticationManager authenticationManager;

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

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        String mobile = parameters.get("mobile");
        String password = parameters.get("password");
        // Protect from downstream leaks of password
        parameters.remove("password");

        Authentication userAuth = new MobileAuthenticationToken(mobile, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        userAuth = authenticationManager.authenticate(userAuth);
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
        }

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

2.4 自定义AuthenticationProvider

package com.chl.auth.security.mobile;

import com.chl.auth.security.token.MobileAuthenticationToken;
import com.chl.auth.service.impl.UserDetailServiceFactory;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

@Setter
@Getter
public class MobileAuthenticationProvider implements AuthenticationProvider {
    private UserDetailServiceFactory userDetailsServiceFactory;
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) {
        MobileAuthenticationToken authenticationToken = (MobileAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String password = (String) authenticationToken.getCredentials();
        UserDetails user = userDetailsServiceFactory.getService(authenticationToken).loadUserByMobile(mobile);
        if (user == null) {
            throw new InternalAuthenticationServiceException("手机号或密码错误");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new InternalAuthenticationServiceException("手机号或密码错误");
        }
        MobileAuthenticationToken authenticationResult = new MobileAuthenticationToken(user, password, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

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

2.5 将自定义的MobilePasswordAuthenticationProvider注入IOC容器

package com.chl.auth.security.mobile;

import com.chl.auth.service.impl.UserDetailServiceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;


@Component
public class MobileAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Resource
    private UserDetailServiceFactory userDetailsServiceFactory;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(HttpSecurity http) {
        //mobile provider
        MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
        provider.setUserDetailsServiceFactory(userDetailsServiceFactory);
        provider.setPasswordEncoder(passwordEncoder);
        http.authenticationProvider(provider);
    }
}

2.6 Security的全局配置指定MobileAuthenticationSecurityConfig

@Configuration
@EnableWebSecurity
// 配置基于方法的注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MobileAuthenticationSecurityConfig mobileAuthenticationSecurityConfig;

    /**
     * 安全策略
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated() //其他请求需要登录
                .and()
                .formLogin()
                .and().apply(mobileAuthenticationSecurityConfig)
                // 解决不允许显示在iframe的问题
                .and().headers().frameOptions()
                .disable().cacheControl();
    }

2.7 加到CompositeTokenGranter集合中

重构configure(AuthorizationServerEndpointsConfigurer endpoints)方法tokenGranter

package com.chl.auth.config;

/**
 * 授权配置
 *
 * @author chlin
 */
@Configuration
public class MyAutorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    /**
     * 认证管理器
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore tokenStore;

    /**
     * 令牌访问端点
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        //将自定义的授权类型添加到tokenGranters中
        List<TokenGranter> tokenGranters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
        tokenGranters.add(new MobilePwdGranter(authenticationManager,tokenServices(),clientDetailsService,new DefaultOAuth2RequestFactory(clientDetailsService)));

        endpoints
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices()) //授权码模式
                .tokenServices(tokenServices()) //令牌管理服务
                .tokenGranter(new CompositeTokenGranter(tokenGranters))
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }


    /**
     * 设置授权码模式如何存储
     *
     * @return  认证服务
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        // 内存方式
//        return new InMemoryAuthorizationCodeServices();
        //jdbc方式 数据库表oauth_code
        return new JdbcAuthorizationCodeServices(dataSource);
    }


    /**
     * 管理令牌
     * @return 认证服务
     */
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);
        //令牌存储策略
        services.setTokenStore(tokenStore);
        //是否允许刷新令牌
        services.setSupportRefreshToken(true);

        //令牌有效期2个小时
        services.setAccessTokenValiditySeconds(7200);
        //刷新令牌有效期3天
        services.setRefreshTokenValiditySeconds(259200);

        //使用jwt令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(jwtAccessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);

        return services;
    }
}

2.8 oauth_client_details表中添加授权类型

oauth_client_details这个表是存储客户端的详细信息的,需要在对应的客户端资源那一行中的authorized_grant_types这个字段中添加自定义的授权类型mobile_password,多个用逗号分隔。

三、测试

3.1 接口

POST http://localhost:8080/api-uaa/oauth/token

参数

header

Authorization Basic d2ViYXBwOjEyMzQ1Ng==

params

grant_type:mobile_password
mobile:18560012618
password:123456

3.2 返回值

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjAyMjUxNTcsInVzZXJfbmFtZSI6IjE4NTYwMDEyNjE4IiwiYXV0aG9yaXRpZXMiOlsicjIiLCJyMyIsInIxIl0sImp0aSI6ImU0Nzk5NjA4LThhYWMtNDk4Yi1iNzVjLWQ1NTk0OGQ3NjZmMiIsImNsaWVudF9pZCI6IndlYmFwcCIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSIsImFsbCJdfQ.YrqIASyRQ42Fo0xDguBcXd1Qzk7Nb-nRc3jGG4AtMRI",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiIxODU2MDAxMjYxOCIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSIsImFsbCJdLCJhdGkiOiJlNDc5OTYwOC04YWFjLTQ5OGItYjc1Yy1kNTU5NDhkNzY2ZjIiLCJleHAiOjE2NjA0NDExNTcsImF1dGhvcml0aWVzIjpbInIyIiwicjMiLCJyMSJdLCJqdGkiOiI0NjFmYzAyNS1iNDU2LTQ4M2MtOWM0MC0xM2E5YmZkOTdjZDQiLCJjbGllbnRfaWQiOiJ3ZWJhcHAifQ.aA1cy48Aalt0Z6ZPOAaD23A-LKq0wK3wNhC0JhN1Lw8",
    "expires_in": 43199,
    "scope": "read write all",
    "jti": "e4799608-8aac-498b-b75c-d55948d766f2"
}
posted @ 2022-08-11 14:05  攻城狮~2022  阅读(442)  评论(0编辑  收藏  举报
所有内容都是自己使用过程的总结,如有不严谨或者不正确的地方,麻烦大家留言指出,一起研讨。