认证授权-1
认证授权
├── 1 spring security框架
│ ├── 1.1 认证Demo
│ │ ├── (1) 添加依赖
│ │ ├── (2) 测试controller
│ │ ├── (3) 安全管理配置 (WebSecurityConfig)
│ │ │
│ ├── 1.2 授权Demo
│ │ ├── (1) 测试controller
│ │ ├── (2) 安全管理配置 (WebSecurityConfig)
│ │ │
├── 2 OAuth2协议
│ ├── 2.1 认证流程
│ │ │
│ ├── 2.2 授权模式
│ │ ├── (1) 授权码模式
│ │ │ ├── 授权服务器配置 (AuthorizationServer)
│ │ │ ├── 令牌配置 (TokenConfig)
│ │ │ ├── 配置认证管理bean (WebSecurityConfig)
│ │ │ ├── 测试
│ │ ├── (2) 密码模式
│ │ │
├── 3 JWT令牌
│ ├── 3.1 当前问题
│ │ │
│ ├── 3.2 jwt组成
│ │ ├── (1) Header
│ │ ├── (2) Payload
│ │ ├── (3) Signature
│ │ │
│ ├── 3.3 认证服务生成jwt
│ │ ├── (1) 令牌配置 (TokenConfig)
│ │ ├── (2) 申请令牌
│ │ ├── (3) 校验jwt令牌
│ │ │
│ ├── 3.4 资源服务校验jwt
│ │ ├── (1) 添加依赖
│ │ ├── (2) 新增令牌配置 (TokenConfig)
│ │ ├── (3) 资源服务配置 (ResouceServerConfig)
│ │ ├── (4) 测试
│ │ │
│ ├── 3.5 获取用户身份
│ │ ├── (1) springSecurity获取
│ │ ├── (2) ThreadLocal获取 (原理)
├── 4 网关认证
│ ├── 4.1 网关的职责
│ │ │
│ ├── 4.2 实现网关认证
│ │ ├── (1) 添加依赖
│ │ ├── (2) 配置类
│ │ │ ├── 令牌配置类 (TokenConfig)
│ │ │ ├── 安全拦截配置类 (SecurityConfig)
│ │ │ ├── 网关认证过滤器 (GatewayAuthFilter)
│ │ │ ├── 错误响应参数包装 (RestErrorResponse)
│ │ ├── (3) 白名单
│ │ ├── (4) 修改资源服务
Spring Security 提供框架支持,OAuth2 用于授权,JWT 用于传递认证信息。
用的是springSecurity框架,基于Oauth2协议实现单点登录
1 spring security框架
1.1 认证Demo
(1) 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
(2) 测试controller
@RestController
public class LoginController {
@Autowired
XcUserMapper userMapper;
@RequestMapping("/login-success")
public String loginSuccess() {
return "登录成功";
}
@RequestMapping("/user/{id}")
public XcUser getuser(@PathVariable("id") String id) {
XcUser xcUser = userMapper.selectById(id);
return xcUser;
}
}
访问http://localhost:63070/auth/user/52
自动进入/login登录页面,/login是spring security提供的
(3) 安全管理配置(WebSecurityConfig)
/**
* @description 安全管理配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置用户信息服务
*/
@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() {
//BCrypt加密方式
//return new BCryptPasswordEncoder();
//密码为明文方式
return NoOpPasswordEncoder.getInstance();
}
/**
* 配置安全拦截机制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
.anyRequest().permitAll()//其它请求全部放行
.and()
.formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
访问http://localhost:64410/auth/user/52 ---->可以正常访问
访问http://localhost:64410/auth/r/r1 ---->未登录则显示登录界面,登录后则可以访问
1.2 授权Demo
(1) 测试controller
在controller中配置**/r/r1需要p1权限**,/r/r2需要p2权限
@RequestMapping("/r/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
public String r1() {
return "访问r1资源";
}
@RequestMapping("/r/r2")
@PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
public String r2() {
return "访问r2资源";
}
(2) 安全配置管理(WebSecurityConfig)
在WebSecurityConfig类配置zhangsan拥有p1权限,lisi拥有p2权限
//配置用户信息服务
@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;
}
2 OAuth2协议
2.1 认证流程
微信扫码认证是基于OAuth2协议实现
2.2 授权模式
(1) 授权码模式
使用授权码去获取令牌(授权码的获取需要资源拥有者亲自授权同意才可以获取)
- 授权服务器配置(AuthorizationServer)
添加**@EnableAuthorizationServer**注解
继承AuthorizationServerConfigurerAdapter来配置OAuth2授权服务器
/**
* @description 授权服务器配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Resource(name = "authorizationServerTokenServicesCustom")
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private AuthenticationManager authenticationManager;
//客户端详情服务
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory() // 使用in-memory存储
.withClient("XcWebApp") // client_id
.secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥(BCrypt加密)
.resourceIds("xuecheng-plus") //资源列表
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all") // 允许的授权范围
.autoApprove(false) //false跳转到授权页面
.redirectUris("http://www.51xuecheng.cn"); //客户端接收授权码的重定向地址
}
//令牌端点的访问配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//认证管理器
.tokenServices(authorizationServerTokenServices)//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//令牌端点的安全配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients() //表单认证(申请令牌)
;
}
}
- 令牌配置(TokenConfig)
/**
* @description 令牌配置
**/
@Configuration
public class TokenConfig {
@Autowired
TokenStore tokenStore;
@Bean
public TokenStore tokenStore() {
//使用内存存储令牌(普通令牌)
return new InMemoryTokenStore();
}
//令牌管理服务
@Bean(name="authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
- 配置认证管理bean(WebSecurityConfig)
在WebSecurityConfig类配置
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
- 测试
-
get请求获取授权码
GET http://localhost:64410/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn
• client_id:客户端准入标识。
• response_type:授权码模式固定为code。
• scope:客户端权限。
• redirect_uri:跳转uri(当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码))
-
请求成功,重定向至http://www.51xuecheng.cn/?code=授权码
(例:http://www.51xuecheng.cn/?code=Wqjb5H)
-
使用httpClient工具发送POST申请令牌
POST http://localhost:64410/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=Wqjb5H&redirect_uri=http://www.51xuecheng.cn
• client_id:客户端准入标识。
• client_secret:客户端秘钥。
• grant_type:授权类型,填写authorization_code,表示授权码模式
• code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
• redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
-
申请令牌成功
{ "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2", "token_type": "bearer", "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f", "expires_in": 7199, "scope": "all" }
• access_token,访问令牌,用于访问资源使用。
• token_type,bearer是在RFC6750中定义的一种token类型,在携带令牌访问资源时需要在head中加入bearer 空格 令牌内容
• refresh_token,当令牌快过期时使用刷新令牌可以再次生成令牌。
• expires_in:过期时间(秒)
• scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。
(2) 密码模式
这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client
-
POST请求获取令牌
• client_id:客户端准入标识。
• client_secret:客户端秘钥。
• grant_type:授权类型,填写password表示密码模式
• username:资源拥有者用户名。
• password:资源拥有者密码。
-
申请令牌成功
{ "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2", "token_type": "bearer", "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f", "expires_in": 6806, "scope": "all" }
3 JWT令牌
3.1 当前问题
普通令牌: "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2"
- 步骤4.5.6,携带令牌获取资源时,需要远程请求认证服务校验令牌,令牌合法才返回资源
- 每次获取获取资源都需要远程调用认证服务校验令牌,性能低
3.2 jwt组成
(1) Header
头部:包括令牌的类型及使用的哈希算法
{
"alg": "HS256",
"typ": "JWT"
}
(2) payload
负载:存放有效信息的地方(例如:iss签发者,exp过期时间戳,sub面向的用户,自定义字段等)
不建议存放敏感信息,此部分可以解码还原原始内容
{
"sub": "1234567890",
"name": "456",
"admin": true
}
(3) Signature
-
签名:用于防止jwt内容被篡改
1.使用base64url将前两部分进行编码
2.编码后使用.连接组成字符串
3.最后使用header中声明的签名算法进行签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
// secret:签名所使用的密钥
3.3 认证服务生成jwt
(1) 令牌配置(TokenConfig)
/**
* @description 令牌配置
**/
@Configuration
public class TokenConfig {
// jwt签名
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
// 使用jwt的tokenstore
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//令牌管理服务
@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;
}
}
(2) 申请令牌
(3) 校验jwt令牌
POST http://localhost:64410/auth/oauth/check_token?token=jwt...
/auth/oauth/token
/auth/oauth/check_token
都是spring security框架自带的
//校验结果
{
"aud": [
"xuecheng-plus"
],
"user_name": "zhangsan",
"scope": [
"all"
],
"active": true,
"exp": 1721317102,
"authorities": [
"p1"
],
"jti": "09fa337e-c8d5-44f2-a7b3-9c7b72080b32",
"client_id": "XcWebApp"
}
3.4 资源服务校验jwt
(1) 添加依赖
<!--认证相关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
(2) 新增令牌配置(TokenConfig)
/**
* @description 令牌配置
**/
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
(3) 资源服务配置(ResouceServerConfig)
/**
* @description 资源服务配置
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
//资源服务标识 (=========对应认证服务配置的resourceId=========)
public static final String RESOURCE_ID = "xuecheng-plus";
@Autowired
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()
//所有/r/** 和 /course/**的请求必须认证通过
.antMatchers("/r/**","/course/**").authenticated()
//除了上面配置的之外的请求全部放行
.anyRequest().permitAll()
;
}
}
(4) 测试
### 课程查询
GET http://localhost:64430/content/course/40
Authorization: Bearer JWT令牌...
3.5 获取用户身份
(1) springSecurity获取
底层是用本地线程变量ThreadLocal
//获取当前用户的身份
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
(2) ThreadLocal获取(原理)
拦截器,在接口入口获取user信息,放到线程本地变量
则在controller和service都可以直接从线程本地变量获取用户信息
- 登录用户本地变量
/**
* @Author: ciky
* @Description: 登录用户本地变量
* @DateTime: 2024/9/19 21:18
**/
public class LoginUserContext {
private static final Logger LOG = LoggerFactory.getLogger(LoginUserContext.class);
private static ThreadLocal<UserLoginResp> userThreadLocal = new ThreadLocal<>();
public static UserLoginResp getUser() {
return userThreadLocal.get();
}
public static void setUser(UserLoginResp user) {
LoginUserContext.userThreadLocal.set(user);
}
public static Long getId(){
try {
return userThreadLocal.get().getId();
} catch (Exception e) {
LOG.error("获取登录用户信息异常",e);
throw e;
}
}
}
- 用户拦截器
/**
* @Author: ciky
* @Description: 用户拦截器
* @DateTime: 2024/9/19 21:32
**/
@Component
public class UserInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(UserInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取header的tokne参数
String token = request.getHeader("token");
if(StrUtil.isNotBlank(token)){
LOG.info("获取用户登录token:{}",token);
//解析token,转成JSON
JSONObject loginUser = JwtUtil.getJSONObject(token);
LOG.info("当前登录用户:{}",loginUser);
//将json转成UserLoginResp用户登录信息
UserLoginResp user = JSONUtil.toBean(loginUser, UserLoginResp.class);
//存到线程本地变量
LoginUserContext.setUser(user);
}
return true;
}
}
//============== JwtUtil的getJSONObject()方法===================;
/**
* 解析token
*/
public static JSONObject getJSONObject(String token){
GlobalBouncyCastleProvider.setUseBouncyCastle(false);
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
JSONObject payloads = jwt.getPayloads();
payloads.remove(JWTPayload.ISSUED_AT);
payloads.remove(JWTPayload.EXPIRES_AT);
payloads.remove(JWTPayload.NOT_BEFORE);
LOG.info("根据token获取原始内容:{}",payloads);
return payloads;
}
- 配置拦截器生效
/**
* @Author: ciky
* @Description: webMVC配置类
* @DateTime: 2024/9/19 21:43
**/
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/user/hello"
"/user/user/send-code",
"/user/user/login"
);
}
}
- 获取用户信息
//从本地线程获取userId
LoginUserContext.getUser();
4 网关认证
4.1 网关的职责
- 认证(网关不负责授权,只负责认证)
- 路由转发
- 白名单维护
4.2 实现网关认证
(1) 添加依赖
<!--springSecurity-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--Oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
(2) 配置类
- 令牌配置类(TokenConfig)
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
- 安全拦截配置类(SecurityConfig)
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
//安全拦截配置
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll() //允许所有路径的请求无需认证即可访问
.anyExchange().authenticated() //所有其他未匹配的请求都需要经过身份验证
.and().csrf().disable().build(); //禁用 CSRF(跨站请求伪造)保护
}
}
- 网关认证过滤器(GatewayAuthFilter)
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
//白名单
private static List<String> whitelist = null;
//初始化白名单
static {
//加载白名单
try (
InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
) {
Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
whitelist= new ArrayList<>(strings);
} catch (Exception e) {
log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
e.printStackTrace();
}
}
@Autowired
private TokenStore tokenStore;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("进入过滤器了");
//请求的URL
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
//检查token是否存在
String token = getToken(exchange);
if (StringUtils.isBlank(token)) {
return buildReturnMono("没有认证",exchange);
}
//判断是否是有效的token
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
boolean expired = oAuth2AccessToken.isExpired();
if (expired) {
return buildReturnMono("认证令牌已过期",exchange);
}
return chain.filter(exchange);
} catch (InvalidTokenException e) {
log.info("认证令牌无效: {}", token);
return buildReturnMono("认证令牌无效",exchange);
}
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
String jsonString = JSON.toJSONString(new RestErrorResponse(error));
byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
- 错误响应参数包装(RestErrorResponse)
public class RestErrorResponse implements Serializable {
private String errMessage;
public RestErrorResponse(String errMessage){
this.errMessage= errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
}
(3) 白名单
security-whitelist.properties
/**=临时全部放行
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口
/checkcode/**=验证码服务接口
(4) 修改资源服务
将资源服务的ResouceServerConfig中认证请求相关的注释掉
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
//.antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll();
}