前言:
全文仅讲解spring security框架的密码模式,通过密码登录获得accessToken和refresh_token的登录方式可以单独使用也可以作为微服务的认证服务,涉及spring boot、spring cloud等框架,应用有redis、oauth2协议、微信登录等。
大纲
一、建表
二、配置文件
2.1 继承 WebSecurityConfigurerAdapter
2.2 资源服务器实现类
2.3 授权服务器配置类
2.4 redisConfig
2.5 DefaultAuthenticationKeyGenerator实现类
三、自定义登录逻辑
3.1 UserDetailsService自定义登录逻辑
3.2.UserDetails登录对象
3.3 登出
3.4 微信登录
四、权限
五、源码及文档
依赖:
仅仅只说 spring security的依赖,security的依赖两个,一个redis依赖(一般要加,不知道不加可不可以),版本自己根据需要定义,正常项目开发需要spring-cloud-alibaba、spring-cloud、spring-boot的依赖,依赖会和nacos等...依赖冲突,可以使用maven Helper解决依赖冲突。idea-setting-Plugins-installed然后搜索maven Helper即可,如果需要图片可以百度:idea下载maven Helper查询搜索即可。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
一、建表
建表的步骤,可以由配置文件代替
创建oauth_client_details
client_id:客户端ID;
client_secret:客户端密钥,在表中需要 BCryptPasswordEncoder() 加密后的字段;
scope:作用域,客户端作用域,不同客户端权限不同;
authorized_grant_types:授权类型,填 authorization_code,password,refresh_token。code码认证模式、密码模式、支持refresh_token;
access_token_validity:token的有效期
refresh_token_validity:refresh_token的有效期
...其他可以不填
参数举例:
{
"username":"1500117839@qq.com",
"password":"admin",
"clientId":"system",
"clientSecret":"system",
"scope":"all",
"grantType":"password"
}
// grantType : 表示认证模式是密码模式
sql如下:
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
二、配置文件:
配置WebSecurityConfigurerAdapter实现类、授权服务器配置类、资源服务器实现类
2.1 继承 WebSecurityConfigurerAdapter
源码说认证用户身份,是要继承WebSecurityConfigurerAdapter的,同时实现configure的方法。
passwordEncoder() 里面定义了密码编码方式(BCryptPasswordEncoder的加密算法),passwordEncoder() 是通过动态盐的方式加密,相同密码每一次加密的结果不一样,但是可以校验,encode()加密方法、match()校验方法。注意: 没有解密的方法,不能解密!!!
关闭跨域限制:http.csrf().disable()
放行的接口及静态资源:http.authorizeRequests().antMatchers().permitAll()
其他的接口全部认证:.anyRequest().authenticated()
放行表格登录.formLogin().permitAll()(可以不写)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 授权服务管理
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/oauth/login" ,"/oauth/weChat/qr"
,"/register/we_chat**","/register/**"
, "/doc.html" ,"/swagger-ui.html/**","/webjars/**","/swagger-resources/**"
,"/"
,"//v2/api-docs","/csrf"
,"/agent/*","user/**"
).permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
2.2 资源服务器实现类
资源服务器,可以不写,不写的时候spring security的校验是通过WebSecurityConfigurerAdapter 的实现类实现校验。写了校验会在资源服务器中校验,校验规则如下:
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/oauth/login"
,"/oauth/weChat/qr","/register/we_chat**","/register/**"
,"/**"
, "/doc.html","/swagger-ui.html/**","/webjars/**","/swagger-resources/**"
,"/","//v2/api-docs","/csrf"
).permitAll()
.anyRequest().authenticated()
;
}
}
2.3 授权服务器配置类
继承 AuthorizationServerConfigurerAdapter
authenticationManager:授权管理器,正常情况只用注入即可;
tokenStore(重点):令牌存储的地方,注入RedisConfig类中的redisTokenStore()方法,redisTokenStore()该方法将DefaultAuthenticationKeyGenerator的实现类MyAuthenticationKeyGenerator,set进redisTokenStore中,保证每个相同的用户每次刷新的token是不一样的,保证了每次登录后token的时间都是最新的,如果不定义则每次登录的用户的token在失效前时间是不会重新更新的。
dataSource:定义了数据源,简单点说就是你是mysql就定义mysql,oracle就定义oracle,这部分根据自己的来,百度上都有。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private LoginUserServiceImpl userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
@Qualifier("redisTokenStore")
private TokenStore tokenStore;
@Resource
private DataSource dataSource;
//这个是定义授权的请求的路径的Bean
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//自定义登录逻辑
.userDetailsService(userService)
//授权管理器
.authenticationManager(authenticationManager)
//令牌存储的地方
.tokenStore(tokenStore)
;
}
}
2.4 redisConfig
两部分:
1.定义redisTokenStore()
2.定义redisTemplate,使用redis和通过redis储存token需要的必要配置。
/**
* 同已用户每次获取token,获取到的都是同一个token,只有token失效后才会获取新token。
* 同一用户每次获取token都生成一个完成周期的token并且保证每次生成的token都能够使用(多点登录)。
* 同一用户每次获取token都保证只有最后一个token能够使用,之前的token都设为无效(单点token)。
* 这里使用第二种:参考https://www.cnblogs.com/cq-yangzhou/p/13207069.html
* @Auther: liuYiZhao
* @Date: 2021-08-06 - 18:02
*/
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore() {
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
redisTokenStore.setAuthenticationKeyGenerator(new MyAuthenticationKeyGenerator());
return redisTokenStore;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// // 使用Jackson2JsonRedisSerialize 替换默认序列化
// Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//
// ObjectMapper objectMapper = new ObjectMapper();
// objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//
// jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setDefaultSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setEnableDefaultSerializer(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2.5 DefaultAuthenticationKeyGenerator实现类
自定义DefaultAuthenticationKeyGenerator实现类,保证每个相同的用户每次刷新的token是不一样的,保证了每次登录后token的时间都是最新的,如果不定义则每次登录的用户的token在失效前时间是不会重新更新的。
/**
* @Auther: liuYiZhao
* @Date: 2021-08-18 - 11:18
*/
public class MyAuthenticationKeyGenerator extends DefaultAuthenticationKeyGenerator {
private static final String CLIENT_ID = "client_id";
private static final String SCOPE = "scope";
private static final String USERNAME = "username";
@Override
public String extractKey(OAuth2Authentication authentication) {
Map<String, String> values = new LinkedHashMap<String, String>();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
//在用户名后面添加时间戳,使每次的key都不一样
values.put(USERNAME, authentication.getName()+System.currentTimeMillis());
}
values.put(CLIENT_ID, authorizationRequest.getClientId());
if (authorizationRequest.getScope() != null) {
values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
}
return generateKey(values);
}
}
三、自定义登录逻辑
3.1 UserDetailsService(重点):自定义登录逻辑,可以设置登录方式,如:微信登录、手机登录、邮箱登录等。
3.2.UserDetails(重点):登录对象。
3.3 登出。
3.1 UserDetailsService(重点):校验的方式只能通过loadUserByUsername()方法的参数username,其中可以根据username这个唯一标识,设置你期望的密码或其他的登录方式。要注意:返回的UserDetails是用户对象,可以继承使用自己的逻辑,也可以不继承使用spring security给你封装的User类。下例:我是返回了继承UserDetails的自定义LoginUser对象。
@Service
@Slf4j
public class LoginUserServiceImpl implements UserDetailsService, LoginUserService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
YucUser yucUser = null;
String substring = username.substring(username.length() - 7);
//微信登录
if (substring.equals("-weChat")) {
String[] split = username.split("-");
return new LoginUser(split[0], passwordEncoder.encode(split[0]), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
//账号密码登录,从用户关系表中找到用户登录凭证
if (PatternUtil.isEmail(username)) {//邮箱登录
String userCode = yucUserRelationsService.findUserCodeByOnly(ParamUtil.C_B_EMAIL, username);
if (userCode == null || userCode.equals("")) {
YucUser yucUserByRegType = yucUserService.findYucUserByRegType(ParamUtil.C_EMAIL, username);
userCode = yucUserByRegType.getUserCode();
}
yucUser = yucUserService.findYucUserByOnly(YucUserService.KEY_USER_CODE, userCode);
} else if (PatternUtil.isMobileNo(username)) {//手机登录
String userCode = yucUserRelationsService.findUserCodeByOnly(ParamUtil.C_B_PHONE, username);
if (userCode == null || userCode.equals("")) {
YucUser yucUserByRegType = yucUserService.findYucUserByRegType(ParamUtil.C_PHONE, username);
userCode = yucUserByRegType.getUserCode();
}
yucUser = yucUserService.findYucUserByOnly(YucUserService.KEY_USER_CODE, userCode);
}
return new LoginUser(username, yucUser.getEngyptPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
4.2 UserDetails:
下例:LoginUser继承自定义YucUser用户类,可以方便自定义YucUser用户对象的属性,实现UserDetails是为了定义出上面,自定义登录逻辑的用户登录的类。
public class LoginUser extends YucUser implements UserDetails, Serializable {
private String username;
private String password;
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
/**
* 账号已过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用
* @return
*/
@Override
public boolean isEnabled() {
return true;
// return getYn();
}
}
4.3 登出
spring security框架封装ConsumerTokenServices接口,用于清除redis保存Token类型的方法。下例中:同时也清除保存在redis中的用户信息。
@Autowired
private ConsumerTokenServices consumerTokenServices;
public Boolean logout( ) {
boolean flag = consumerTokenServices.revokeToken(accessToken);
Boolean delete = redisOperateManager.deleteString("access_token:" + accessToken);
return flag && delete;
}
注意:
如果想清除非redis保存的令牌,spring security框架TokenStore接口中的removeAccessToken方法中,有相应的实现类(如下图),同时也可以自己继承后定义登录逻辑。
3.4 微信登录
四、权限
五、spring 官方文档
5.1 spring security 官方文档列出的过滤链,从上到下的排序。
未写完待定……