OAuth 2.0授权框架支持第三方支持访问有限的HTTP服务,通过在资源所有者和HTTP服务之间进行一个批准交互来代表资源者去访问这些资源,或者通过允许第三方应用程序以自己的名义获取访问权限。
主要配置继承AuthorizationServerConfigurerAdapter的三个配置,其他的属于配置类中方法或参数的加强和修饰。
package com.panda.admin.login.authconfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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.ClientDetailsService;
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.redis.RedisTokenStore;
import java.util.Arrays;
/**
* author: tcy
* createDate: 2022/10/26
* description:认证授权服务配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private SysUserDetailServiceImpl sysUserDetailService;
/**
* redis工厂
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 用来配置令牌的安全约束
* 拦截控制
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.checkTokenAccess("permitAll()") // check_token公开
.tokenKeyAccess("permitAll()") // token_key公开
.allowFormAuthenticationForClients(); // 表单认证(申请令牌)
}
/**
* 用来配置客户端详情服务(ClientDetailsService)
* 允许客户端自己申请ClientID
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/**
* 数据库中添加客户端信息表,可以启用withClientDetails
* 还需要自己建立ClientDetailsService的实现类ClientDetailsServiceImpl
* ClientDetailsServiceImpl implements ClientDetailsService
* 在实现类中将客户端相关数据配置好
*/
//clients.withClientDetails(clientDetailsService);
/**
* 客户端配置唯一,直接在代码中配置也可以
* 但是保密性不强,安全性不高
*/
clients.inMemory()
.withClient("client") //注册客户端id
.secret(passwordEncoder.encode("secret")) //注册客户端密码
.accessTokenValiditySeconds(60) //有效时间60秒,测试阶段容易看出效果
.authorizedGrantTypes("authorization_code","password","refresh_token") //三方登录、密码授权、刷新令牌
.autoApprove(true) //登录后绕过批准询问(/oauth/confirm_access)
.scopes("all") //允许授权范围
.redirectUris("https://gitee.com/"); //回调网址,携带授权码
}
/**
* 管理令牌:用来配置令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//token增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
/** refresh token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
* 1 重复使用:access token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
* 2 非重复使用:access token过期刷新时, refresh token过期时间延续,在refresh token有效期内刷新便永不失效达到无需再次登录的目的
*/
.reuseRefreshTokens(true)
.userDetailsService(sysUserDetailService);
}
/**
* 自定义的Token增强器,把更多信息放入Token中
* 登录者的id、角色、手机号、其他绑定账号等
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return new EnhanceTokenEnhancer();
}
/**
* 配置JWT令牌
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
//设置jwt的转换器
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
/**
* 非对称加密:密钥库中获取密钥对(公钥+私钥)
* 需要 *.jks文件
*/
//KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jks/jwt.jks"), "123456".toCharArray());
//KeyPair keyPair = keyStoreKeyFactory.getKeyPair("jwt","888888".toCharArray()); //设置获取秘钥的密码
//converter.setKeyPair(keyPair); //设置秘钥对象
/**
* 对称加密
*/
converter.setSigningKey("Tu");
return converter;
}
/**
* JWT令牌存储
*/
@Bean
public TokenStore tokenStore() {
/**
* springSecurity中TokenStore实现有4种,分别是:
* InMemoryTokenStore(保存本地内存)
* JdbcTokenStore(保存到数据库)
* JwkTokenStore(全部信息返回到客户端)
* RedisTokenStore(保存到redis)
*/
//存放至redis库,注意pom和yml中相关配置
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
redisTokenStore.setPrefix("auth:token:access:");
return redisTokenStore;
//return new JwtTokenStore(jwtAccessTokenConverter());
}
}
主要为密码、身份认证、请求放行
package com.panda.admin.login.authconfig;
import com.panda.admin.login.auth.service.impl.SysUserDetailServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
/**
* author: tcy
* createDate: 2022/10/27
* description:安全配置
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private SysUserDetailServiceImpl sysUserDetailService;
/**
* 密码编码器
*
* 委托方式,根据密码的前缀选择对应的encoder,
* 例如:{bcypt}前缀->标识BCYPT算法加密;
* {noop}->标识不使用任何加密即明文的方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
//秘文编辑密码
//return new BCryptPasswordEncoder();
//编辑工厂,可用多种方式编辑
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* 认证管理器
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 身份验证管理器
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 测试用户,单独提供
* 只能用这个用户才能登录
*/
//auth.inMemoryAuthentication()
// .withUser("panda")
// .password(passwordEncoder().encode("123456"))
// .roles("admin");
/**
* 这里从数据库查询用户,但是登录方式唯一
*/
//auth.userDetailsService(sysUserDetailService);
/**
* 可以匹配多种登录方式
* 账号密码、手机验证码、第三方授权等
*/
auth.authenticationProvider(daoAuthenticationProvider());
}
/**
* 账号密码 认证授权提供者
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(sysUserDetailService);
provider.setPasswordEncoder(passwordEncoder());
provider.setHideUserNotFoundExceptions(false); //是否隐藏用户不存在异常
return provider;
}
/**
* 配置放行规则
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/login/**").permitAll()
.antMatchers("/webjars/**", "/doc.html", "/swagger-resources", "/v2/api-docs","swagger-ui.html").permitAll()
.antMatchers("/actuator/**","/instances/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
@Override
@Bean
public UserDetailsService userDetailsService(){
return super.userDetailsService();
}
}
这部分可以直接放在AuthorizationServerConfig的token增强器中
package com.panda.admin.login.authconfig;
import com.panda.admin.login.constant.SecurityConstants;
import org.springframework.security.core.Authentication;
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 java.util.HashMap;
import java.util.Map;
/**
* author: tcy
* createDate: 2022/10/26
* description:Token增强器
*/
public class EnhanceTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
if (userAuthentication != null) {
Map additionalInfo = new HashMap<>();
additionalInfo.put(SecurityConstants.DETAILS_USER_ID, 2629738803L);
additionalInfo.put(SecurityConstants.DETAILS_USERNAME, "承天永明");
additionalInfo.put(SecurityConstants.DETAILS_NICKNAME, "飒飒");
additionalInfo.put(SecurityConstants.OPEN_ID, "123456789");
additionalInfo.put(SecurityConstants.ROLE_ID, "1");
additionalInfo.put(SecurityConstants.MOBILE, "19108217140");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
}
return oAuth2AccessToken;
}
}
package com.panda.admin.login.auth.Details;
import com.panda.admin.login.SystemUser;
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.io.Serializable;
import java.util.Collection;
/**
* author: tcy
* createDate: 2022/11/9
* description:系统管理用户认证信息
*/
@Data
public class SysUserDetails implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String nickName;
/**
* UserDetails默认字段
*/
private String username;
private String password;
private Collection authorities;
public SysUserDetails(SystemUser user){
this.setId(user.getId());
this.setUsername(user.getNickname());
//数据表中,账号为account字段
this.setUsername(user.getAccount());
this.setPassword(user.getPassword());
}
@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;
}
}
package com.panda.admin.login.auth.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.panda.admin.login.SystemUser;
import com.panda.admin.login.auth.Details.SysUserDetails;
import com.panda.admin.result.ServiceException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
/**
* author: tcy
* createDate: 2022/11/9
* description:系统用户信息
*/
@Service
public class SysUserDetailServiceImpl implements UserDetailsService {
@Resource
private SystemUserServiceImpl systemUserService;
/**
* 账号密码 认证
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserDetails sysUserDetails = null;
//查询用户存在
SystemUser systemUser = this.systemUserService.lambdaQuery().eq(SystemUser::getAccount, username).one();
if (ObjectUtil.isNotEmpty(systemUser)){
sysUserDetails = new SysUserDetails(systemUser);
}
if (ObjectUtils.isEmpty(systemUser) || ObjectUtils.isEmpty(sysUserDetails)){
throw new ServiceException("用户不存在");
}
return sysUserDetails;
}
}
/token/oauth接口是oauth2配置自带,不用建立相关接口,只需要模块的本地接口对上
(1)在用户表中创建用户数据,密码为明文
(2)在系统用户信息SysUserDetailServiceImpl中添加以下内容,{noop}表示不加密
if (ObjectUtil.isNotEmpty(systemUser)){
systemUser.setPassword("{noop}" + systemUser.getPassword());
sysUserDetails = new SysUserDetails(systemUser);
}
(3)使用apifox,配置用户信息,配置客户端信息,然后发送请求
得到的结果:
(1)建立秘文接口
package com.panda.admin.login.auth.controller;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* author: tcy
* createDate: 2022/11/9
* description:
*/
@Api(tags = "安全")
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/info")
public String info(){
String secret = passwordEncoder.encode("tcy510823");
return secret;
}
}
(2)apifox中建立相关请求,将第6步获取的token放在请求头Authorization中,注意:bearer + token。
(3)复制秘文密码,在数据库中建立用户数据
(1)去掉SysUserDetailServiceImpl中的不加密操作
systemUser.setPassword("{noop}" + systemUser.getPassword());
(2)更换用户信息发送请求
(1)/oauth/check_token为oauth2自带接口
(2)可以加工,得到适合的反馈效果
@Api(tags = "oauth登录安全")
@RestController
@RequestMapping("/oauth")
public class SystemUserController {
@Autowired
TokenStore tokenStore;
@ApiOperation(value = "校验token")
@GetMapping("/check_token")
public Result checkToken(@RequestParam("token") String token) {
OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(token);
if (ObjectUtils.isEmpty(oAuth2AccessToken) || oAuth2AccessToken.isExpired()){
throw new ServiceException("token无法识别或已经过期");
}
OAuth2Authentication oAuth2Authentication = this.tokenStore.readAuthentication(token);
if (ObjectUtils.isEmpty(oAuth2Authentication)){
throw new ServiceException("验证信息无效或已过期");
}
int expiresIn = oAuth2AccessToken.getExpiresIn();
return Result.success("token校验通过,有效时间:" + expiresIn + "s");
}
}
(3)复制上面得到的token,放入请求参数中
(4)结果
(1)使用http://localhost:9001/oauth/token接口
(2)grant_type需要换成refresh_token
(3)refresh_token的值为之前请求,获取的refresh_token
(4)结果,注意:"expires_in": 59