参考:https://blog.youkuaiyun.com/qq_30359369/article/details/103329481
https://blog.youkuaiyun.com/yx444535180/article/details/119924836
OAuth(开发授权)是一个开放标准,允许用户授权第三方应用,访问他们存储在另外的服务提供者上的信息。(OAuth1.0已被完全废止)。
OAuth2主要用于校验客户端合法性、产生token、校验token
因此OAuth2与Sercurity整合之后,校验顺序:
校验客户端合法性——校验用户名密码——产生token——校验token——校验接口权限
OAuth2 协议
介绍
引入依赖
spring-cloud-starter-oauth2依赖了spring-cloud-starter-security
但也可以两个都引入,配置它版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
添加配置类
BeanConfig
package com.collect.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
public class BeanConfig {
private String SIGNING_KEY = "mq123";
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
}
注入配置的Bean
TokenConfig
package com.collect.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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 java.util.Arrays;
/**
* @author Administrator
* @version 1.0
**/
@Configuration
public class TokenConfig {
@Autowired
TokenStore tokenStore;
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
//令牌管理服务
@Bean(name="authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
配置令牌的相关策略,比如设置密钥、有效期、刷新令牌有效期
WebSecurityConfig
package com.collect.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
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.method.configuration.EnableGlobalMethodSecurity;
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.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import javax.annotation.Resource;
/**
* @author Mr.M
* @version 1.0
* @description 安全管理配置
* @date 2022/9/26 20:53
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置用户信息服务
// @Bean
// public UserDetailsService userDetailsService() {
// //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
// manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
// return manager;
// }
@Bean
public PasswordEncoder passwordEncoder() {
// //密码为明文方式
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
@Autowired
private DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
.anyRequest().permitAll()//其它请求全部放行
.and()
.formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
}
public static void main(String[] args) {
String password = "111111";
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
for (int i = 0; i < 5; i++) {
//生成密码
String encode = passwordEncoder.encode(password);
System.out.println(encode);
//校验密码,参数1是输入的明文 ,参数2是正确密码加密后的串
boolean matches = passwordEncoder.matches(password, encode);
System.out.println(matches);
}
boolean matches = passwordEncoder.matches("1234", "$2a$10$fb2RlvFwr9HsRu9vH1OxCu/YiMRw6wy5UI6u3s0A.0bVSuR1UqdHK");
System.out.println(matches);
}
}
配置secuity的拦截规则、密码的验证方式
oauth 协议常用的两种授权模式测试
授权码模式
本文不演示授权码模式
密码模式
密码模式相对授权码模式简单,授权码模式需要借助浏览器供用户亲自授权,密码模式不用借助浏览器,如下图:
POST请求获取令牌
服务器地址 /oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=shangsan&password=123
参数列表如下:
• client_id:客户端准入标识。
• grant_type:授权类型,填写password表示密码模式
• username:资源拥有者用户名。
• password:资源拥有者密码。
client_id、grant_type 均可在此配置动态设置
使用密码模式授权:https://blog.youkuaiyun.com/danfeng827/article/details/138351701
实现统一认证
统一认证包括:
- 普通的认证
- 第三方认证
第三方认证不需要校验密码
所以要实现统一认证,需要重写 DaoAuthenticationProvider 类里的additionalAuthenticationChecks 方法,不需要让它进行密码校验,我们自己进行即可
package com.collect.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
/**
* @author Mr.M
* @version 1.0
* @description 重写了DaoAuthenticationProvider的校验的密码的方法,因为我们统一认证入口,有一些认证方式不需要校验密码
* @date 2023/2/24 11:40
*/
@Component
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
}
设置成我们自己的 DaoAuthenticationProvider 对象即可
@Autowired
private DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
通过工厂模式实现统一认证
新增一个 AuthService 接口
package com.collect.ucenter.service;
import com.collect.ucenter.model.dto.AuthParamsDto;
import com.collect.ucenter.model.dto.XcUserExt;
/**
* @author Mr.M
* @version 1.0
* @description 统一的认证接口
* @date 2023/2/24 11:55
*/
public interface AuthService {
/**
* @description 认证方法
* @param authParamsDto 认证参数
* @return com.xuecheng.ucenter.model.po.XcUser 用户信息
* @author Mr.M
* @date 2022/9/29 12:11
*/
XcUserExt execute(AuthParamsDto authParamsDto);
}
后续需要认证时只需要继承该接口,并对bean名称按固定规则起名
package com.collect.ucenter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.collect.ucenter.feignclient.CheckCodeClient;
import com.collect.ucenter.mapper.XcUserMapper;
import com.collect.ucenter.model.dto.AuthParamsDto;
import com.collect.ucenter.model.dto.XcUserExt;
import com.collect.ucenter.model.po.User;
import com.collect.ucenter.service.AuthService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* @author Mr.M
* @version 1.0
* @description 账号名密码方式
* @date 2023/2/24 11:56
*/
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
@Autowired
XcUserMapper xcUserMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
CheckCodeClient checkCodeClient;
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
//账号
String username = authParamsDto.getUsername();
//输入的验证码
String checkcode = authParamsDto.getCheckcode();
//验证码对应的key
String checkcodekey = authParamsDto.getCheckcodekey();
if(StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)){
throw new RuntimeException("请输入的验证码");
}
//远程调用验证码服务接口去校验验证码
Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
if(verify == null || !verify){
throw new RuntimeException("验证码输入错误");
}
//账号是否存在
//根据username账号查询数据库
User xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
//查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
if(xcUser==null){
throw new RuntimeException("账号不存在");
}
//验证密码是否正确
//如果查到了用户拿到正确的密码
String passwordDb = xcUser.getPassword();
//拿 到用户输入的密码
String passwordForm = authParamsDto.getPassword();
//校验密码
boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
if( !matches ){
throw new RuntimeException("账号或密码错误");
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
return xcUserExt;
}
}
前端在传参时增加一个 authType 认证类型的参数,通过该参数拼接得到不同 Bean 对应的实现类
@Autowired
ApplicationContext applicationContext;
//认证类型,有password,wx。。。
String authType = authParamsDto.getAuthType();
//根据认证类型从spring容器取出指定的bean
String beanName = authType+"_authservice";
AuthService authService = applicationContext.getBean(beanName, AuthService.class);
密码模式校验逻辑
package com.collect.ucenter.service.impl;
import com.alibaba.fastjson.JSON;
import com.collect.ucenter.mapper.XcMenuMapper;
import com.collect.ucenter.mapper.XcUserMapper;
import com.collect.ucenter.model.dto.AuthParamsDto;
import com.collect.ucenter.model.dto.XcUserExt;
import com.collect.ucenter.model.po.XcMenu;
import com.collect.ucenter.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
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.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2023/2/24 10:37
*/
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {
@Resource
XcMenuMapper xcMenuMapper;
@Autowired
ApplicationContext applicationContext;
//传入的请求认证的参数就是AuthParamsDto
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//将传入的json转成AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
throw new RuntimeException("请求认证参数不符合要求");
}
//认证类型,有password,wx。。。
String authType = authParamsDto.getAuthType();
//根据认证类型从spring容器取出指定的bean
String beanName = authType+"_authservice";
AuthService authService = applicationContext.getBean(beanName, AuthService.class);
//调用统一execute方法完成认证
XcUserExt xcUserExt = authService.execute(authParamsDto);
//封装xcUserExt用户信息为UserDetails
//根据UserDetails对象生成令牌
UserDetails userPrincipal = getUserPrincipal(xcUserExt);
return userPrincipal;
}
/**
* @description 查询用户信息
* @param xcUser 用户id,主键
* @return com.xuecheng.ucenter.model.po.XcUser 用户信息
* @author Mr.M
* @date 2022/9/29 12:19
*/
public UserDetails getUserPrincipal(XcUserExt xcUser){
String password = xcUser.getPassword();
//权限
String[] authorities= {};
//根据用户id查询用户的权限
List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(xcUser.getId());
if(xcMenus.size()>0){
List<String> permissions =new ArrayList<>();
xcMenus.forEach(m->{
//拿到了用户拥有的权限标识符
permissions.add(m.getCode());
});
//将permissions转成数组
authorities = permissions.toArray(new String[0]);
}
xcUser.setPassword(null);
//将用户信息转json
String userJson = JSON.toJSONString(xcUser);
UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build();
return userDetails;
}
}
当请求密码模式接口时:继承 UserDetailsService 接口并实现 loadUserByUsername 方法,OAUTH2 底层会调用我们实现的方法
配置资源服务器
一般的场景是使用密码模式授权成功后会返回一个令牌,此时我们要通过此令牌来访问其他的资源服务,OAuth 给我们提供了资源服务器的配置以及它自己的拦截器,只要按照它的方式加上请求头就可以绕过。
只需要在资源服务器加上这两个配置类即可
SIGNING_KEY 一定要跟认证服务的配置类对应的 SIGNING_KEY 一样
package com.collect.manage_file.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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 java.util.Arrays;
/**
* @author Administrator
* @version 1.0
**/
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
package com.collect.manage_file.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* @description 资源服务配置
* @author Mr.M
* @date 2022/10/18 16:33
* @version 1.0
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
//资源服务标识
public static final String RESOURCE_ID = "collect_url";
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers().permitAll() //放行的地址
.anyRequest().authenticated() //除了配置放行的地址其他的所有请求必须认证通过
;
}
}
最主要是加上了 EnableResourceServer 这个注解,标识了这是资源服务器
参考:https://cloud.tencent.com/developer/article/2264103
https://www.cnblogs.com/wuzhenzhao/p/13232530.html
https://juejin.cn/post/7160554181156306958#heading-6
带上令牌的请求头格式请遵守: 请求头名称:Authorization,内容前缀为 Bearer 后接一个空格,最后再加上令牌
流程图
最后,整个流程图如下:
此图解释的是正确的流程,如果失败将会在那一步返回错误信息