前言
本文需要读者具备一定的Oauth2.0相关的技术支持,以下不多阐述
登录功能是项目中必不可少的一环,本文以Spring Security Oauth2 + JWT为例实现密码登录,验证码登录和授权码登录获取token并以gateway作为资源服务器统一验证和鉴权的功能。
一、生成jks证书
可以用jdk自带的工具keytool生成私钥公钥的文件,即jks证书,再后续的jwt加密方式和token的验证方式需要用到
生成方式:
管理员身份打开jdk的bin目录执行一下命令
-alias: 别名
-keyalg: 指定密钥的算法
-keypass:指定别名条目的密码(私钥的密码)
-keystore:指定密钥库的名称
-storepass:指定密钥库的密码
keytool -genkeypair -alias jwt -keyalg RSA -keypass 1354361838 -keystore jwt.jks -storepass 1354361838
生成文件如下:
将此文件复制到授权模块的resources下面
二、授权(auth)模块(密码登录)
项目版本控制
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<spring.cloud.alibaba.version>2.2.8.RELEASE</spring.cloud.alibaba.version>
<spring.cloud.version>Hoxton.SR12</spring.cloud.version>
部分依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
1.配置授权服务
记得添加注解@EnableAuthorizationServer
package com.bs.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
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 org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @author bingshao
* @date 2022/9/8
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AuthenticationManager authenticationManager;
@Resource
private UserDetailsService userDetailsServiceImpl;
@Resource
private JwtTokenEnhancer jwtTokenEnhancer;
@Resource
private DataSource dataSource;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.inMemory()
// .withClient("bs-client")
// .secret(passwordEncoder.encode("1354361838"))
// .scopes("all")
// .authorizedGrantTypes("sms", "authorization_code", "client_credentials", "implicit", "password", "refresh_token")
// .accessTokenValiditySeconds(7200)
// .refreshTokenValiditySeconds(86400)
// .redirectUris("https://bingshao.cool");
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// token增强
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates);
// 添加自定义授权方式
ArrayList<TokenGranter> tokenGranterList = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
tokenGranterList.add(new SmsTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranterList);
endpoints.authenticationManager(authenticationManager)
// 默认的授权方式+自定义的方式
.tokenGranter(compositeTokenGranter)
// 配置加载用户信息的服务
.userDetailsService(userDetailsServiceImpl)
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain)
// 允许所有方式
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public KeyPair keyPair() {
//从classpath下的证书中获取秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "1354361838".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "1354361838".toCharArray());
}
}
- AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter类,重写了三个方法,分别是
-
public void configure(AuthorizationServerSecurityConfigurer security)
此方法是设置端点的安全,即设置一些端点的访问权限,permitAll()表示全部允许 -
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
配置端点的一些配置,如上述代码的token增强器和新加一些授权方式 -
public void configure(ClientDetailsServiceConfigurer clients)
此方法是设置客户端的信息,可以信息放在内存里,也可以使用数据源的方式,正常是要放在数据库的用以区分客户端和一些配置信息。若使用数据源的方式,系统自带的建表语句如下,直接导入即可:
CREATE TABLEoauth_client_details
(
client_id
varchar(255) NOT NULL COMMENT ‘客户端唯一标识’,
client_name
varchar(255) DEFAULT NULL COMMENT ‘客户端名称’,
resource_ids
varchar(255) DEFAULT NULL COMMENT ‘客户端所能访问的资源id集合,多个资源时用逗号(,)分隔’,
client_secret
varchar(255) DEFAULT NULL COMMENT ‘客户端密钥’,
scope
varchar(255) DEFAULT NULL COMMENT ‘客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔’,
authorized_grant_types
varchar(255) NOT NULL COMMENT ‘客户端支持的授权许可类型(grant_type),可选值包括authorization_code,password,refresh_token,implicit,client_credentials,若支持多个授权许可类型用逗号(,)分隔’,
web_server_redirect_uri
varchar(255) DEFAULT NULL COMMENT ‘客户端重定向URI,当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与数据库内的redirect_uri是否一致’,
authorities
varchar(255) DEFAULT NULL COMMENT ‘客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔’,
access_token_validity
int(11) DEFAULT NULL COMMENT ‘设定客户端的access_token的有效时间值(单位:秒),若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时)’,
refresh_token_validity
int(11) DEFAULT NULL COMMENT ‘设定客户端的refresh_token的有效时间值(单位:秒),若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天)’,
additional_information
varchar(4096) DEFAULT NULL COMMENT ‘这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据’,
autoapprove
varchar(255) DEFAULT ‘false’ COMMENT ‘设置用户是否自动批准授予权限操作, 默认值为 ‘false’, 可选值包括 ‘true’,‘false’, ‘read’,‘write’.’,
PRIMARY KEY (client_id
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘oauth2客户端表’
-
新增一条记录
-
public KeyPair keyPair()
注入获取秘钥的方法 -
public JwtAccessTokenConverter accessTokenConverter()
注入jwt转换器 -
public TokenStore tokenStore()
设置token的存储方式,实现方式有三种,InMemoryTokenStore
,JwtTokenStore
,RedisTokenStore
,本文运用jwt的方式进行存储
2.加密方式
package com.bs.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @author bingshao
* @date 2022/9/8
**/
@Component
public class PasswordEncoderComponent {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.token增强器
package com.bs.auth.config;
import com.bs.auth.vo.UserVo;
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.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
/**
* @author bingshao
* @date 2022/9/8
**/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
UserVo userVo = (UserVo) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>(16);
info.put("id", userVo.getId());
try {
// 中文只能加解密处理,否则乱码
info.put("username", URLEncoder.encode(userVo.getUsername(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
可以将一些需要的不重要的字段放在token里面进行业务操作
4.安全配置
记得添加@EnableWebSecurity
package com.bs.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
/**
* @author bingshao
* @date 2022/9/8
**/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsServiceImpl;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private SmsAuthenticationProvider smsAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(smsAuthenticationProvider)
.userDetailsService(userDetailsServiceImpl)
.passwordEncoder(passwordEncoder);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.antMatchers("/test/**").permitAll()
.anyRequest().authenticated()
.and()
// .httpBasic(); //基础对话框
.formLogin(); //formLogin登录
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
-
WebSecurityConfigurerAdapter 也有三个重写方法
-
protected void configure(AuthenticationManagerBuilder auth)
配置授权管理器的管理方法,加密方式,自定义授权方式等等 -
public void configure(WebSecurity web)
-
protected void configure(HttpSecurity http)
配置哪些请求直接允许访问,哪些需要认证,.permitAll()前面的就是放行
-
-
protected AuthenticationManager authenticationManager()
注入认证管理器,密码模式和自定义的一些验证码等等查询数据库的登录需要注入此容器
5.实现UserDetailsService方法
模拟下数据库登录,正常应该通过feign调用
package com.bs.auth.service.impl;
import com.bs.auth.utils.Assert;
import com.bs.auth.vo.UserVo;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import javax.annotation.Resource;
import java.util.LinkedList;
import java.util.List;
/**
* @author bingshao
* @date 2022/9/8
**/
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserVo userVo = new UserVo();
// 模拟数据库查询
if ("bingshao".equals(username)) {
// 普通用户,权限为4
userVo.setId(1L);
userVo.setUsername("bingshao");
userVo.setPassword(passwordEncoder.encode("123456"));
List<SimpleGrantedAuthority> list = new LinkedList<>();
list.add(new SimpleGrantedAuthority("4"));
userVo.setAuthorities(list);
} else if ("superadmin".equals(username)) {
// 超级管理员,权限为1,2,3,4
userVo.setId(2L);
userVo.setUsername("superadmin");
userVo.setPassword(passwordEncoder.encode("123456"));
List<SimpleGrantedAuthority> list = new LinkedList<>();
list.add(new SimpleGrantedAuthority("1"));
list.add(new SimpleGrantedAuthority("2"));
list.add(new SimpleGrantedAuthority("3"));
list.add(new SimpleGrantedAuthority("4"));
userVo.setAuthorities(list);
} else {
Assert.isError("查无此人");
}
return userVo;
}
}
UserVo类代码:
package com.bs.auth.vo;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @author bingshao
* @date 2022/9/8
**/
@Data
public class UserVo implements UserDetails {
private Long id;
private Collection<SimpleGrantedAuthority> authorities;
private String password;
private String username;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
6. 密码登录测试
三、源码分析
封装TokenRequest对象,具体细节不展示了
中间进行一些列的检验,不细说,下面的方法比较关键
我们进grant方法看一下
tokenGranters有两个,其中有一个是我们后续自己写的短信token授予,系统自带的tokenGranters我们看到是有五个,分别是
授权码模式AuthorizationCodeTokenGranter
,
密码模式ResourceOwnerPasswordTokenGranter
,
简易模式ImplicitTokenGranter
,
客户端模式ClientCredentialsTokenGranter
,
刷新模式RefreshTokenGranter
循环匹配授权模式,由于我们是密码模式,所以需要使用ResourceOwnerPasswordTokenGranter进行授予
看图进的是AbstractTokenGranter的grant方法,因为系统五种授权模式都是集成的这个抽象类,并且密码模式自己自己没有重写这个方法,所以调用的是父类的grant方法。
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
此方法通过代理获取到我们数据库的配置的客户端的信息
validateGrantType(grantType, client);方法就是就行授权方式的校验,
执行clientDetails.getAuthorizedGrantTypes();方法可以看到我们后续的短信授权模式已经在系统里面的,这个地方之所以会出现,是因为我们在WebSecurityConfigurerAdapter的子类进行了配置,如下
代码继续走到getAccessToken(ClientDetails client, TokenRequest tokenRequest)方法
注意上图的getOAuth2Authentication(client, tokenRequest)方法,此方法子类进行了重写,所以会走子类的方法,不走父类的,我们看看子类的方法怎么写的
可以看到即取出字段信息并封装成Authentication对象,重要点,在这里去除了参数中的密码信息,然后看重点方法authenticate,看名字就很重点,认证方法,关键来了!
进入authenticate方法我们来到了这里
此时的provider是DaoAuthenticationProvider,在进入此方法到了AbstractUserDetailsAuthenticationProvider的authenticate方法,可能有人会问为什么不是DaoAuthenticationProvider的authenticate方法。跟上面道理一样,DaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的子类并且自身没有重写authenticate方法,所以用的是父类的同名方法。
继续看上图红圈代码,我们进retrieveUser方法看看
绕了一圈终于看到我们熟悉的代码了,loadUserByUsername,此时调用的就是我们自定义的实现UserDetailsService方法的实现类UserDetailsServiceImpl文件里面loadUserByUsername方法的认证
到这里其实验证这块就差不多了,后续的就是校验对象和一些token颁发的规则(包括我们自己的token增强器)就不细看了。
四、验证码登录
在上面源码分析中我们也看到了几个关键的类,AuthenticationProvider(身份验证提供者),AbstractAuthenticationToken(授权token),AbstractTokenGranter(token颁发)。我们只需要实现或者继承这些类在写自己的逻辑即可。
1.SmsTokenGranter
package com.bs.auth.config;
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 javax.annotation.Resource;
import java.io.Serializable;
import java.util.Map;
/**
* @author bingshao
* @date 2022/9/13
**/
public class SmsTokenGranter extends AbstractTokenGranter implements Serializable {
private static final long serialVersionUID = -5700526092268849704L;
private static final String GRANT_TYPE = "sms";
// private ClientDetailsService clientDetailsService;
@Resource
private AuthenticationManager authenticationManager;
public SmsTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected SmsTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
// this.clientDetailsService = clientDetailsService;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = tokenRequest.getRequestParameters();
String phone = parameters.get("phone");
String code = parameters.get("code");
Authentication userAuth = new SmsAuthenticationToken(phone, code);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
} 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 phone/code 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 phone: " + phone);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
// @Override
// public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
// String clientId = tokenRequest.getClientId();
// ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
// return getAccessToken(client, tokenRequest);
// }
}
2.SmsAuthenticationToken
package com.bs.auth.config;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serializable;
import java.util.Collection;
/**
* @author bingshao
* @date 2022/9/13
**/
public class SmsAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
private static final long serialVersionUID = -5728457041722914996L;
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 {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
3.SmsAuthenticationProvider
package com.bs.auth.config;
import com.bs.auth.utils.Assert;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import javax.annotation.Resource;
/**
* @author bingshao
* @date 2022/9/14
**/
@Configuration
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Resource
private UserDetailsService msgDetailsServiceImpl;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
String phone = (String) authenticationToken.getPrincipal();
String code = (String) authenticationToken.getCredentials();
// 模拟比对验证码
if (!"123456".equals(code)) {
Assert.isError("验证码错误");
}
UserDetails userDetails = msgDetailsServiceImpl.loadUserByUsername(phone);
// 此处身份已经确认
return createSuccessAuthentication(userDetails, authentication);
}
private Authentication createSuccessAuthentication(UserDetails userDetails, Authentication authentication) {
SmsAuthenticationToken result = new SmsAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
4.在授权服务进行配置
在public void configure(AuthorizationServerEndpointsConfigurer endpoints)方法里面添加自定义授权方式
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// token增强
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates);
// 添加自定义授权方式
ArrayList<TokenGranter> tokenGranterList = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
tokenGranterList.add(new SmsTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranterList);
endpoints.authenticationManager(authenticationManager)
// 默认的授权方式+自定义的方式
.tokenGranter(compositeTokenGranter)
// 配置加载用户信息的服务
.userDetailsService(userDetailsServiceImpl)
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain)
// 允许所有方式
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
5.安全配置
在protected void configure(AuthenticationManagerBuilder auth)方法添加上我们自己定义的授权身份提供者
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(smsAuthenticationProvider)
.userDetailsService(userDetailsServiceImpl)
.passwordEncoder(passwordEncoder);
}
6.新写实现类MsgDetailsServiceImpl
此实现类仅用于短信
package com.bs.auth.service.impl;
import com.bs.auth.utils.Assert;
import com.bs.auth.vo.UserVo;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import javax.annotation.Resource;
import java.util.LinkedList;
import java.util.List;
/**
* @author bingshao
* @date 2023/2/23
**/
@Service("msgDetailsServiceImpl")
public class MsgDetailsServiceImpl implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserVo userVo = new UserVo();
if ("17751335300".equals(username)) {
// 普通用户,权限为1,2
userVo.setId(1L);
userVo.setUsername("妃妃");
userVo.setPassword(passwordEncoder.encode("123456"));
List<SimpleGrantedAuthority> list = new LinkedList<>();
list.add(new SimpleGrantedAuthority("1"));
list.add(new SimpleGrantedAuthority("2"));
userVo.setAuthorities(list);
return userVo;
} else {
Assert.isError("查无此人");
}
return userVo;
}
}
7.测试效果
五、gateway作为验证和资源服务器
为了减轻各个服务的压力,我们选择在gateway进行token的验证处理,和资源权限的鉴别。
1.部分依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
2.配置文件
server:
port: 1112
spring:
profiles:
active: dev
application:
name: bs-gateway
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:1111/rsa/publicKey' #配置RSA的公钥访问地址
redis:
host: localhost
timeout: 10000
port: 6379
password: 1354361838
database: 0
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能
lower-case-service-id: true #使用小写服务名,默认是大写
routes: #配置路由路径
- id: bs-auth-route
uri: lb://bs-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: bs-demo-route
uri: lb://bs-demo
predicates:
- Path=/demo/**
filters:
- StripPrefix=1
secure:
ignore:
urls: #配置白名单路径
- "/auth/oauth/token"
- "/auth/oauth/authorize"
# client:
# login-url: "/auth/oauth/token"
# client-id: bs-client
# client-secret: 1354361838
3.资源管理
package com.bs.gataway.config;
import cn.hutool.core.util.ArrayUtil;
import com.bs.common.constant.AuthConstant;
import com.bs.gataway.handler.CustomServerAccessDeniedHandler;
import com.bs.gataway.handler.CustomServerAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
/**
* @author bingshao
* @date 2022/9/20
**/
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
@Resource
private AuthorizationManager authorizationManager;
@Resource
private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
@Resource
private CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
@Resource
private IgnoreUrlsConfig whiteListConfig;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
// 自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);
http.authorizeExchange()
// 白名单配置
.pathMatchers(ArrayUtil.toArray(whiteListConfig.getUrls(), String.class)).permitAll()
// 鉴权管理器配置
.anyExchange().access(authorizationManager)
.and()
.exceptionHandling()
// 处理未授权
.accessDeniedHandler(customServerAccessDeniedHandler)
//处理未认证
.authenticationEntryPoint(customServerAuthenticationEntryPoint)
.and().csrf().disable();
return http.build();
}
/**
* ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
* 需要把jwt的Claim中的authorities加入
* 方案:重新定义ReactiveAuthenticationManager权限管理器,默认转换器JwtGrantedAuthoritiesConverter
*/
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
4.授权管理
package com.bs.gataway.config;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.bs.common.constant.AuthConstant;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author bingshao
* @date 2022/9/20
**/
@Component
@Log4j2
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
String path = request.getURI().getPath();
// PathMatcher pathMatcher = new AntPathMatcher();
String roleIds = stringRedisTemplate.opsForValue().get(AuthConstant.USER_ROLE + path);
// 获取当前url需要的角色权限,可以将url与role的对应关系存入redis,再通过url获取到所需角色id,下面再判断该账号角色有无包含上述redis中所取到的id
List<String> authorities = StrUtil.isNotBlank(roleIds) ? Arrays.stream(roleIds.split(",")).map(role -> AuthConstant.AUTHORITY_PREFIX + role).collect(Collectors.toList()) : new LinkedList<>();
// 为空说明没做限制,都能访问
boolean isPublic = CollectionUtil.isEmpty(authorities);
Mono<AuthorizationDecision> authorizationDecisionMono = authentication
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(roleId -> {
if (isPublic) {
return true;
}
// roleId是请求用户的角色(格式:ROLE_{roleId}),authorities是请求资源所需要角色的集合
log.info("访问路径:{}", path);
log.info("用户角色roleId:{}", roleId);
log.info("资源需要权限authorities:{}", authorities);
return authorities.contains(roleId);
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
return authorizationDecisionMono;
}
}
5.忽略的url
package com.bs.gataway.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author bingshao
* @date 2022/9/20
**/
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix="secure.ignore")
public class IgnoreUrlsConfig {
private List<String> urls;
}
6.全局过滤器
package com.bs.gataway.filter;
import cn.hutool.core.util.StrUtil;
import com.bs.common.constant.RequestHeaderConstant;
import com.nimbusds.jose.JWSObject;
import lombok.SneakyThrows;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 将登录用户的JWT转化成用户信息的全局过滤器
*
* @author bingshao
* @date 2022/9/20
**/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HttpHeaders headers = exchange.getRequest().getHeaders();
String token = headers.getFirst(RequestHeaderConstant.AUTHORIZATION);
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
String realToken = token.replace(RequestHeaderConstant.TOKENHEAD, "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
ServerHttpRequest request = exchange.getRequest().mutate().header(RequestHeaderConstant.USER, userStr).build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
7.没有访问权限的处理器
package com.bs.gataway.handler;
import cn.hutool.json.JSONUtil;
import com.bs.common.utils.Result;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* 没有访问权限
*
* @author bingshao
* @date 2022/9/20
**/
@Component
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getHeaders().set("Access-Control-Allow-Origin", "*");
response.getHeaders().set("Cache-Control", "no-cache");
String body = JSONUtil.toJsonStr(Result.forbidden(e.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
8.token校验失败的处理器
package com.bs.gataway.handler;
import cn.hutool.json.JSONUtil;
import com.bs.common.utils.Result;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* token校验失败(错误或者过期)
*
* @author bingshao
* @date 2022/9/20
**/
@Component
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
String body = JSONUtil.toJsonStr(Result.unauthorized(e.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
9.通过gateway调用服务测试
测试个token错误的
总结
git地址https://gitee.com/bingshao0412/learning-induction.git
,喜欢的朋友点个星星,后续会持续更新。不足之处望指教~