从实战的角度谈微服务(九):对于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"
}
本篇内容是参考网络教程学习过程中的笔记
开发工作着,生活快乐着,留下总结,相互交流,共同进步
开发工作着,生活快乐着,留下总结,相互交流,共同进步