SpringSecurity(二十二)--OAuth2:实现资源服务器(下)通过redis和缓存实现TokenStore

news2024/11/15 11:42:19

一、前言

本章将在前面几章基础上进行讲解,所以大家最好尽量先去看一下前几章的内容再来跟进会好很多。那么本章我们将通过redis和本地缓存Caffeine对JdbcTokenStore进行重写,并且讲解资源服务器配置的新方案,使得我们可以不用在资源服务器又配置一个和授权服务器一样的tokenStore.
我们上一章实现了JdbcTokenStore,那么大家都知道,redis的速度是肯定的比普通的数据库是要快的,且JdbcTokenStore实在是有点难拓展,尤其涉及到表结构的更改,所以选择使用Redis对TokenStore进行重写。但是大量的请求,且token基本都是长信息,肯定也是会对Redis造成不小的压力,所以这里使用了本地缓存Caffeine。那么这里的写法我是照着下面这篇博客写的,我看了很多重写的方法,基本只有这篇我仿写后能够正常运行。再次也对这篇博客作者表示感谢,真的强!

https://www.cnblogs.com/chongsha/p/14558011.html

后面有时间和能力后我会阅读OAuth2源码结合之前写的代码做几期分享

二、实现JsonRedisTokenStore

在说实现之前,先来说一下为什么要重写这个类,其实懂一点的开发同仁会说,欸,OAuth2不是提供了RedisTokenStore类吗,为啥还要重写?
确实,OAuth2给我们提供了RedisTokenStore类,并且使用上比JdbcTokenStore还简单,我们只需要配置好redis以及在授权服务器类注入RedisConnectFactory就可以直接配置RedisTokenStore。
在这里插入图片描述
但是你或许也看到了,默认的RedisTokenStore采用的默认序列化方式是JDK序列化。
在这里插入图片描述
学过Redis的应该了解了,这会引起存储的对象产生乱码问题,所以我们需要使用json对Authentication和token进行序列化,这就是我们重写的原因,且我们可以在其基础上加入本地缓存减小Redis的压力。

首先在授权服务器项目上我们需要redis和Caffeine,fastJson的依赖,大家如果有自己想用的本地缓存也可以直接换,例如仍然使用Redis实现本地缓存也是Ok的:

       <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

然后在yaml文件需要对redis做相关配置:

spring:
  # redis配置
  redis:
    host: 127.0.0.1
    password: 123456
    port: 6379

然后就可以重写了,关于方法的大致解释我已经做了相关注释,大家直接看着一块儿块儿自己写就好,我们这儿
JsonRedisTokenStore.java

package com.mbw.security.token;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.mbw.pojo.Role;
import com.mbw.pojo.User;
import com.mbw.security.dto.JwtUserDto;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class JsonRedisTokenStore implements TokenStore {
	private static final String ACCESS = "access:";
	private static final String AUTH_TO_ACCESS = "auth_to_access:";
	private static final String AUTH = "auth:";
	private static final String REFRESH_AUTH = "refresh_auth:";
	private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
	private static final String REFRESH = "refresh:";
	private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
	private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
	private static final String UNAME_TO_ACCESS = "uname_to_access:";

	private final static Cache<Object, Object> CACHE;

	static {
		CACHE = Caffeine.newBuilder()
				.expireAfterWrite(60, TimeUnit.SECONDS)
				.maximumSize(1000).build();
	}


	private final StringRedisTemplate stringRedisTemplate;
	private final AuthenticationKeyGenerator authenticationKeyGenerator = new CustomAuthenticationKeyGenerator();


	public JsonRedisTokenStore(StringRedisTemplate stringRedisTemplate) {
		this.stringRedisTemplate = stringRedisTemplate;
	}


	@Override
	public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
		return this.readAuthentication(token.getValue());
	}

	//根据AccessToken对象查询对应的OAuth2Authentication(认证的用户信息)
	@Override
	public OAuth2Authentication readAuthentication(String token) {
		String key = AUTH + token;

		return (OAuth2Authentication) loadCache(key, (k) -> {
			String json = stringRedisTemplate.opsForValue().get(key);
			if (StrUtil.isBlank(json)) {
				return null;
			}
			return fullParseJSON(json);
		});
	}

	/**
	 * 完整的OAuth2Authentication 对象转换
	 *
	 * @param json 完整OAuth2Authentication json字符串
	 * @return OAuth2Authentication对象
	 */
	private OAuth2Authentication fullParseJSON(String json) {
		JSONObject jsonObject = JSONObject.parseObject(json);

		JSONObject userAuthenticationObject = jsonObject.getJSONObject("userAuthentication");
		/**
		 * 这里通过之前写的UserDetails的构造方法,我的UserDetails由三部分构造:User类,Set<Role>,List<String> authorityName
		 * 所以这里你可以直接按照你自己的userDetails来进行json解析
		 * 而UserDetails在这里就是Principal部分,所以json得先解析principal才能得到我需要的UserDetails组件
		 */
		
		User userInfo = userAuthenticationObject.getJSONObject("principal").getObject("user",User.class);
		Set<Role> roleInfo = new HashSet<>(userAuthenticationObject.getJSONObject("principal").getJSONArray("roleInfo").toJavaList(Role.class));
		List<String> authorityNames = userAuthenticationObject.getJSONObject("principal").getJSONArray("authorityNames").toJavaList(String.class);
		JwtUserDto jwtUserDto = new JwtUserDto(userInfo, roleInfo, authorityNames);
		String credentials = userAuthenticationObject.getString("credentials");
		JSONObject detailsJSONObject = userAuthenticationObject.getJSONObject("details");
		LinkedHashMap<String, Object> details = new LinkedHashMap<>();
		for (String key : detailsJSONObject.keySet()) {
			details.put(key, detailsJSONObject.get(key));
		}

		UsernamePasswordAuthenticationToken userAuthentication = new UsernamePasswordAuthenticationToken(jwtUserDto
				, credentials, new ArrayList<>(0));
		userAuthentication.setDetails(details);

		JSONObject storedRequest = jsonObject.getJSONObject("oAuth2Request");
		String clientId = storedRequest.getString("clientId");

		JSONObject requestParametersJSON = storedRequest.getJSONObject("requestParameters");
		Map<String, String> requestParameters = new HashMap<>();
		for (String key : requestParametersJSON.keySet()) {
			requestParameters.put(key, requestParametersJSON.getString(key));
		}

		Set<String> scope = convertSetString(storedRequest, "scope");
		Set<String> resourceIds = convertSetString(storedRequest, "resourceIds");
		Set<String> responseTypes = convertSetString(storedRequest, "responseTypes");

		OAuth2Request oAuth2Request = new OAuth2Request(requestParameters
				, clientId
				//由于这个项目不需要处理权限角色,所以就没有对权限角色集合做处理
				, new ArrayList<>(0)
				, storedRequest.getBoolean("approved")
				, scope
				, resourceIds
				, storedRequest.getString("redirectUri")
				, responseTypes
				, null //extensionProperties
		);

		return new OAuth2Authentication(oAuth2Request, userAuthentication);
	}

	private static Set<String> convertSetString(JSONObject data, String key) {
		List<String> list = data.getJSONArray(key).toJavaList(String.class);

		return new HashSet<>(list);
	}

	//存储accessToken并且存储用户认证信息(Principal)
	@Override
	public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
		String serializedAccessToken = JSONObject.toJSONString(token);
		String serializedAuth = JSONObject.toJSONString(authentication);
		String accessKey = ACCESS + token.getValue();
		String authKey = AUTH + token.getValue();
		String authToAccessKey = AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication);
		String approvalKey = UNAME_TO_ACCESS + getApprovalKey(authentication);
		String clientId = CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId();

		int seconds = 30 * 24 * 60 * 60;
		if (token.getExpiration() != null) {
			seconds = (int) DateUtil.between(new Date(),token.getExpiration(), DateUnit.SECOND);
		}

		try {
			stringRedisTemplate.opsForValue().set(accessKey, serializedAccessToken);
			stringRedisTemplate.opsForValue().set(authKey, serializedAuth);
			stringRedisTemplate.opsForValue().set(authToAccessKey, serializedAccessToken);

			if (!authentication.isClientOnly()) {
				stringRedisTemplate.opsForHash().putIfAbsent(approvalKey, token.getValue(), serializedAccessToken);
			}
		} finally {
			//如果中途失败,则还可以补偿过期时间
			stringRedisTemplate.expire(accessKey, seconds, TimeUnit.SECONDS);
			stringRedisTemplate.expire(authKey, seconds, TimeUnit.SECONDS);
			stringRedisTemplate.expire(authToAccessKey, seconds, TimeUnit.SECONDS);
			stringRedisTemplate.expire(clientId, seconds, TimeUnit.SECONDS);
			stringRedisTemplate.expire(approvalKey, seconds, TimeUnit.SECONDS);
		}

		OAuth2RefreshToken refreshToken = token.getRefreshToken();
		if (refreshToken != null && refreshToken.getValue() != null) {
			String refreshValue = token.getRefreshToken().getValue();
			String refreshToAccessKey = REFRESH_TO_ACCESS + refreshValue;
			String accessToRefreshKey = ACCESS_TO_REFRESH + token.getValue();

			try {
				stringRedisTemplate.opsForValue().set(refreshToAccessKey, token.getValue());
				stringRedisTemplate.opsForValue().set(accessToRefreshKey, refreshValue);
			} finally {
				//如果中途失败,则还可以补偿过期时间
				refreshTokenProcess(refreshToken, refreshToAccessKey, accessToRefreshKey);
			}

			CACHE.put(refreshToAccessKey, token.getValue());
			CACHE.put(accessToRefreshKey, refreshValue);
		}

		CACHE.put(accessKey, token);
		CACHE.put(authKey, authentication);
		CACHE.put(authToAccessKey, token);
	}

	private void refreshTokenProcess(OAuth2RefreshToken refreshToken, String refreshKey, String refreshAuthKey) {
		int seconds = 30 * 24 * 60 * 60;
		if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
			ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
			Date expiration = expiringRefreshToken.getExpiration();

			int temp;
			if (expiration != null) {
				temp = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
						.intValue();

			} else {
				temp = seconds;
			}
			stringRedisTemplate.expire(refreshKey, temp, TimeUnit.SECONDS);
			stringRedisTemplate.expire(refreshAuthKey, temp, TimeUnit.SECONDS);
		}
	}


	private String getApprovalKey(OAuth2Authentication authentication) {
		String userName = "";
		if (authentication.getUserAuthentication() != null) {
			JwtUserDto userInfoDetails = (JwtUserDto) authentication.getUserAuthentication().getPrincipal();
			userName = userInfoDetails.getUser().getMobile() + "_" + userInfoDetails.getUsername();
		}

		return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
	}

	private String getApprovalKey(String clientId, String userName) {
		return clientId + (userName == null ? "" : ":" + userName);
	}


	//根据AccessToken的value值查询对应的token对象
	@Override
	public OAuth2AccessToken readAccessToken(String tokenValue) {
		String key = ACCESS + tokenValue;
		//先从本地缓存取,没有再从redis中取,都没有返回Null
		return (OAuth2AccessToken) loadCache(key, (k) -> {
			String json = stringRedisTemplate.opsForValue().get(key);
			if (StrUtil.isNotBlank(json)) {
				return JSONObject.parseObject(json, DefaultOAuth2AccessTokenEx.class);
			}
			return null;
		});
	}

	@Override
	public void removeAccessToken(OAuth2AccessToken accessToken) {
		removeAccessToken(accessToken.getValue());
	}

	public void removeAccessToken(String tokenValue) {
		String accessKey = ACCESS + tokenValue;
		String authKey = AUTH + tokenValue;
		String accessToRefreshKey = ACCESS_TO_REFRESH + tokenValue;

		OAuth2Authentication authentication = readAuthentication(tokenValue);
		String access = stringRedisTemplate.opsForValue().get(accessKey);

		List<String> keys = new ArrayList<>(6);
		keys.add(accessKey);
		keys.add(authKey);
		keys.add(accessToRefreshKey);

		stringRedisTemplate.delete(keys);

		if (authentication != null) {
			String key = authenticationKeyGenerator.extractKey(authentication);
			String authToAccessKey = AUTH_TO_ACCESS + key;
			String unameKey = UNAME_TO_ACCESS + getApprovalKey(authentication);
			String clientId = CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId();

			stringRedisTemplate.delete(authToAccessKey);
			stringRedisTemplate.opsForHash().delete(unameKey, tokenValue);
			stringRedisTemplate.opsForList().remove(clientId, 1, access);
			stringRedisTemplate.delete(ACCESS + key);

			CACHE.invalidate(authToAccessKey);
			CACHE.invalidate(ACCESS + key);
		}

		CACHE.invalidateAll(keys);
	}

	@Override
	public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
		String refreshKey = REFRESH + refreshToken.getValue();
		String refreshAuthKey = REFRESH_AUTH + refreshToken.getValue();
		String serializedRefreshToken = JSONObject.toJSONString(refreshToken);

		stringRedisTemplate.opsForValue().set(refreshKey, serializedRefreshToken);
		stringRedisTemplate.opsForValue().set(refreshAuthKey, JSONObject.toJSONString(authentication));

		refreshTokenProcess(refreshToken, refreshKey, refreshAuthKey);

		CACHE.put(refreshKey, refreshToken);
		CACHE.put(refreshAuthKey, authentication);
	}


	//和readAccessToken的原理一致
	@Override
	public OAuth2RefreshToken readRefreshToken(String tokenValue) {
		String key = REFRESH + tokenValue;
		return (OAuth2RefreshToken) loadCache(key, (k) -> {
			String json = stringRedisTemplate.opsForValue().get(key);
			if (StrUtil.isNotBlank(json)) {
				return JSONObject.parseObject(json, DefaultOAuth2RefreshTokenEx.class);
			}

			return null;
		});
	}

	@Override
	public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
		return this.readAuthenticationForRefreshToken(token.getValue());
	}

	public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
		String key = REFRESH_AUTH + token;

		return (OAuth2Authentication) loadCache(key, (k) -> {
			String json = stringRedisTemplate.opsForValue().get(key);
			if (StrUtil.isBlank(json)) {
				return null;
			}

			return fullParseJSON(json);
		});
	}

	@Override
	public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
		this.removeRefreshToken(refreshToken.getValue());
	}

	public void removeRefreshToken(String refreshToken) {
		String refreshKey = REFRESH + refreshToken;
		String refreshAuthKey = REFRESH_AUTH + refreshToken;
		String refresh2AccessKey = REFRESH_TO_ACCESS + refreshToken;
		String access2RefreshKey = ACCESS_TO_REFRESH + refreshToken;

		List<String> keys = new ArrayList<>(7);
		keys.add(refreshKey);
		keys.add(refreshAuthKey);
		keys.add(refresh2AccessKey);
		keys.add(access2RefreshKey);

		stringRedisTemplate.delete(keys);

		CACHE.invalidateAll(keys);
	}

	@Override
	public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
		this.removeAccessTokenUsingRefreshToken(refreshToken.getValue());
	}

	private void removeAccessTokenUsingRefreshToken(String refreshToken) {
		String key = REFRESH_TO_ACCESS + refreshToken;

		String accessToken = stringRedisTemplate.opsForValue().get(key);
		stringRedisTemplate.delete(key);

		if (accessToken != null) {
			removeAccessToken(accessToken);
		}

		CACHE.invalidate(key);
	}

	private <T> Object loadCache(String key, Function<Object, ? extends T> loadData) {
		try {
			Object value = CACHE.getIfPresent(key);
			if (value == null) {
				value = loadData.apply(key);
				//如果redis中有则将redis中的token放入本地缓存中
				if (value != null) {
					CACHE.put(key, value);
				}
			}

			return value;
		} catch (Exception e) {
			throw new RuntimeException("JsonRedisTokenStore.loadCache从缓存中加载数据发生错误", e);
		}
	}

	@Override
	public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
		String key = AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication);

		return (OAuth2AccessToken) loadCache(key, (k) -> {
			String json = stringRedisTemplate.opsForValue().get(key);

			if (StrUtil.isNotBlank(json)) {
				DefaultOAuth2AccessToken accessToken = JSONObject.parseObject(json, DefaultOAuth2AccessTokenEx.class);


				OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());

				if (storedAuthentication == null
						|| !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication))) {
					this.storeAccessToken(accessToken, authentication);
				}

				return accessToken;
			}

			return null;
		});
	}



	@Override
	public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
		String approvalKey = UNAME_TO_ACCESS + getApprovalKey(clientId, userName);

		return getOAuth2AccessTokens(approvalKey);
	}

	private Collection<OAuth2AccessToken> getOAuth2AccessTokens(String approvalKey) {
		return (Collection<OAuth2AccessToken>) loadCache(approvalKey, (k) -> {
			Map<Object, Object> accessTokens = stringRedisTemplate.opsForHash().entries(approvalKey);

			if (accessTokens.size() == 0) {
				return Collections.emptySet();
			}

			List<OAuth2AccessToken> result = new ArrayList<>();

			for (Object json : accessTokens.values()) {
				String strJSON = json.toString();
				OAuth2AccessToken accessToken = JSONObject.parseObject(strJSON, DefaultOAuth2AccessTokenEx.class);

				result.add(accessToken);
			}
			return Collections.unmodifiableCollection(result);
		});
	}

	@Override
	public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
		String key = CLIENT_ID_TO_ACCESS + clientId;

		return getOAuth2AccessTokens(key);
	}
}

DefaultOAuth2RefreshTokenEx.java

package com.mbw.security.token;

import lombok.Data;
import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken;
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;

import java.util.Date;

@Data
public class DefaultOAuth2RefreshTokenEx extends DefaultOAuth2RefreshToken implements ExpiringOAuth2RefreshToken {
	private Date expiration;
	private String value;

	public DefaultOAuth2RefreshTokenEx() {
		super(null);
	}
}

DefaultOAuth2AccessTokenEx.java

package com.mbw.security.token;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;

public class DefaultOAuth2AccessTokenEx extends DefaultOAuth2AccessToken {
	private DefaultOAuth2RefreshTokenEx refreshToken;

	public DefaultOAuth2AccessTokenEx() {
		super((String) null);
	}

	@Override
	public void setValue(String value) {
		super.setValue(value);
	}

	@Override
	public DefaultOAuth2RefreshTokenEx getRefreshToken() {
		return refreshToken;
	}

	public void setRefreshToken(DefaultOAuth2RefreshTokenEx refreshToken) {
		this.refreshToken = refreshToken;
	}
}

在项目中使用spring security oauth2做了统一登录授权,在实际开发过程中,发现不同终端同一账号登录,返回的token是一样的。我们使用的是redis存储token,于是查了资料,发现是因为生成token key的算法的原因,导致了多端登录返回一个token的问题,原因如图:
在这里插入图片描述
生成key使用的是DefaultAuthenticationKeyGenerator,代码:

public class DefaultAuthenticationKeyGenerator implements AuthenticationKeyGenerator {

	private static final String CLIENT_ID = "client_id";

	private static final String SCOPE = "scope";

	private static final String USERNAME = "username";

	public String extractKey(OAuth2Authentication authentication) {
		Map<String, String> values = new LinkedHashMap<String, String>();
		OAuth2Request authorizationRequest = authentication.getOAuth2Request();
		if (!authentication.isClientOnly()) {
			values.put(USERNAME, authentication.getName());
		}
		values.put(CLIENT_ID, authorizationRequest.getClientId());
		if (authorizationRequest.getScope() != null) {
			values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
		}
		return generateKey(values);
	}
}

从代码里面看,生成key使用的是 client_id、scope、username三个字段,由于这三个字段同一用户在同一子系统中是不变的,所以导致多端登录时,生成的token key是一样的,就会造成返回的token一样,这样的后果就是,其中一个终端退出登录,所有已登录设备就失效了,于是就重写这extractKey方法,继承这个类,增加了一个device_id字段,从而解决多端登录需要互不干扰的需求:

package com.mbw.security.token;

import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeSet;

public class CustomAuthenticationKeyGenerator extends DefaultAuthenticationKeyGenerator {
	private static final String CLIENT_ID = "client_id";

	private static final String SCOPE = "scope";

	private static final String USERNAME = "username";

	private static final String DEVICE_ID = "device_id";

	@Override
	public String extractKey(OAuth2Authentication authentication) {
		Map<String, String> values = new LinkedHashMap<String, String>();
		OAuth2Request authorizationRequest = authentication.getOAuth2Request();
		if (!authentication.isClientOnly()) {
			values.put(USERNAME, authentication.getName());
		}
		values.put(CLIENT_ID, authorizationRequest.getClientId());
		if (authorizationRequest.getScope() != null) {
			values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
		}

		String deviceId = authorizationRequest.getRequestParameters().get(DEVICE_ID);
		values.put(DEVICE_ID, deviceId);

		return generateKey(values);
	}
}

而我们生成Token的接口的参数就要再加一个device_id,而这个deviceId可以通过存进用户表让前端取到。
在这里插入图片描述
然后就可以在AuthServerConfig配置该类了:

package com.mbw.security.config;

import com.mbw.security.service.ClientDetailsServiceImpl;
import com.mbw.security.service.UserDetailsServiceImpl;
import com.mbw.security.token.JsonRedisTokenStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.token.DefaultTokenServices;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private ClientDetailsServiceImpl clientDetailsServiceImpl;
	@Autowired
	private UserDetailsServiceImpl userDetailsServiceImpl;
	@Autowired
	private JsonRedisTokenStore jsonRedisTokenStore;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager)
		.userDetailsService(userDetailsServiceImpl)
				.tokenStore(jsonRedisTokenStore);
		DefaultTokenServices tokenService = getTokenStore(endpoints);
		endpoints.tokenServices(tokenService);
	}

	//配置TokenService参数
	private DefaultTokenServices getTokenStore(AuthorizationServerEndpointsConfigurer endpoints) {
		DefaultTokenServices tokenService = new DefaultTokenServices();
		tokenService.setTokenStore(endpoints.getTokenStore());
		tokenService.setSupportRefreshToken(true);
		tokenService.setClientDetailsService(endpoints.getClientDetailsService());
		tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
		//token有效期 1小时
		tokenService.setAccessTokenValiditySeconds(3600);
		//token刷新有效期 15天
		tokenService.setRefreshTokenValiditySeconds(3600 * 12 * 15);
		tokenService.setReuseRefreshToken(false);
		return tokenService;
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.withClientDetails(clientDetailsServiceImpl);
	}



	/**
	 * 解决访问/oauth/check_token 403的问题
	 *
	 * @param security
	 * @throws Exception
	 */
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		// 允许表单认证
		security
				.tokenKeyAccess("permitAll()")
				.checkTokenAccess("permitAll()")
				.allowFormAuthenticationForClients();

	}
}

然后现在启动授权服务器调用获取token
在这里插入图片描述
然后回到redis看看成果:
在这里插入图片描述
发现token已经存进redis,并且序列化。

三、资源服务器配置

如果按照上一章的说法,我们接下来要在资源服务器也要配置同样的tokenStore,但是这也是我上一章结尾提到的代码耦合问题,明显不应该这样做。那么怎么解决呢?首先大家需要了解一个额外的配置:user-info-uri。user-info-uri原理是在授权服务器认证后将认证信息Principal通过形参绑定的方法通过URL的方式获取用户信息。所以这个大家可以想象成和token-info-uri类似的作用,只是我们不再直接调用授权服务器而已,而是一种类似职责分离,授权认证交给授权服务器,认证后的用户信息给到资源服务器,资源服务器再提供资源。
那么我们只需要在application.yaml修改如下配置即可:

security:
  oauth2:
    resource:
      id: resource_server
      user-info-uri: http://localhost:9090/api/member
      prefer-token-info: false

与此同时,资源服务器的配置类只需要专注于我们需要保护的资源即可:
ResourceServerConfig.java

package com.mbw.config;

import org.springframework.context.annotation.Configuration;
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 javax.servlet.http.HttpServletResponse;


@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
				.csrf().disable()
				.exceptionHandling()
				.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
				.and()
				.authorizeRequests()
				.antMatchers("/test/**").authenticated()
				.and()
				.httpBasic();
	}

}

那资源服务器怎么知道我要访问哪个tokenStore呢?
还记得如果资源服务器和授权服务器在同一个项目时访问的是同一个bean这个结论吗,那么我们只需要在授权服务器项目上再配置一个资源服务器去保护user-info-uri这个资源即可:

package com.mbw.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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 javax.servlet.http.HttpServletResponse;


@Configuration
@EnableResourceServer
@Order(3)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
				.csrf().disable()
				.exceptionHandling()
				.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
				.and()
				.requestMatchers().antMatchers("/api/**")
				.and()
				.authorizeRequests()
				.antMatchers("/api/**").authenticated()
				.and()
				.httpBasic();
	}
}

而在授权服务器配置的资源服务器是知道用哪个tokenStore的,那么一个调用链就出现了:
①用户带着token访问资源服务器保护的资源,而资源服务器为了获取用户信息,就会去调用user-info-uri,而这个路径被我们在授权服务器配置的资源服务器拦截,因为需要经过身份认证,第一站经过OAuth2AuthenticationProcessingFilter认证处理过滤器:

在这里插入图片描述
此时Authentication中的Principal装着就是token,就类似于短信认证,认证前我们自己封装的Authentication对象Principal认证前放的是短信一个道理
在这里插入图片描述
②然后由AuthenticationManager通过token进行身份验证,熟悉吗,是不是和之前学习的原生Spring Security的身份验证链很像,然后通过tokenService的loadAuthentication进行认证获取认证对象,这里的tokenService大家想象成UserDetailsService就好了。
在这里插入图片描述
③tokenService获取Authentication对象来自DefaultTokenServices类,就是我们授权服务器配置的类:
在这里插入图片描述
那这里的tokenStore就是授权服务器配置的,也就是我们重写的jsonRedisTokenStore:

在这里插入图片描述
最后,来到这个配置的user-info-uri,读取用户认证信息返回给我们真正的授权服务器。
在这里插入图片描述
然后有了用户的认证信息,资源服务器把资源返回给客户端:
在这里插入图片描述
当然这只是我的个人理解,我自己debug了很多遍,由于找不到类似的资料,书上也没有讲解,我只能这样理解。如果大家有更好的想法,可以在评论区分享。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/56842.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]计算机毕业设计springboot校园疫情防范管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

LeetCode 337. 打家劫舍 III(C++)*

该题也是使用动态规划的思路&#xff0c;主要考虑根节点的最大金额和左右子节点的关系&#xff0c;其中分为两种情况&#xff1a;有该结点有没有偷钱&#xff0c;其次要遵守不报警原则。可得到状态转移方程&#xff1a; f为根节点被选中的最大&#xff0c;g为根节点没被选中的最…

Day17-购物车页面-结算-动态计算已勾选商品的数据和选中状态

1.动态渲染已勾选商品的总数量 我的操作&#xff1a; 1》在 store/cart.js 模块中&#xff0c;定义一个名称为 checkedCount 的 getters&#xff0c;用来统计已勾选商品的总数量&#xff1a; 2》在 my-settle 组件中&#xff0c;通过 mapGetters 辅助函数&#xff0c;将需要的…

[附源码]Python计算机毕业设计Django健身房信息管理

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

GIS工具maptalks开发手册(五)01-用JSON载入地图——json格式绘制多个面之基础版

GIS工具maptalks开发手册(五)01-用JSON载入地图——json格式绘制多个面之基础版 效果-json渲染图层基础版 代码 index.html <!DOCTYPE html> <html> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width,…

HTML5期末考核大作业,网站——旅游景点。 学生旅行 游玩 主题住宿网页

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

【网络层】流量控制VS拥塞控制、路由器功能、SDN控制平面

文章目录前言网络层功能流量控制VS拥塞控制拥塞控制路由器功能转发---硬件解决------数据平面---------处理数据各种转发路由选择---软件解决---控制平面----控制网络协议运行-------OSPF、RIP、BGP数据平面控制平面---路由选择传统方法-------每路由器法----------路由选择处理…

[附源码]计算机毕业设计疫苗及注射管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Ubuntu20.04 通过deb包方式安装微信

写在前面 写文时间&#xff1a;2022.12.03 周六 自己的系统是Ubuntu20.04.5&#xff0c;安装的是 weixin_2.1.1_amd64.deb。 安装微信 从优麒麟官网下载微信deb安装包。 下载完成后&#xff0c;直接安装即可 sudo dpkg -i weixin_2.1.1_amd64.deb参考链接 [1] 优麒麟官网…

基于HFSS的线阵综合分析

摘要&#xff1a; 常规的阵列天线方向图综合是基于阵因子分析法&#xff0c;且不考虑单元之间电磁耦合的一种快速分析手段。本次推文则简单阐述一个基于HFSS的线阵综合实例。 HFSS中的直线阵 均匀直线阵的基础知识已在前面的推文中进行了多次阐述举例&#xff0c;这里就不赘…

正则表达式中的元字符,量词:贪婪和非贪婪,转义符: \s: 记得使用-z --null-data: 使用ascii码中空字符来替换新行,分组:““,和‘‘

正则表达式的所有内容&#xff1a;&#xff08;每一个解释下面都带一个样例&#xff09; 1.元字符 \&#xff1a;忽略后面一个字符的特殊含义 [a-b]&#xff1a;对a到b之间的任何字符进行匹配 ^&#xff1a;在每行的开始进行匹配 $ &#xff1a;在每行的末尾进行匹配 . .&…

FFmpeg编译参数分析

config.mak 来传递给 makefile &#xff0c;还会生成 config.h 给 C 程序 include 引入。 由于 configure 脚本的编译参数是非常多的&#xff0c;本文主要讲解一些比较常用的编译参数&#xff0c;一些特殊的编译参数&#xff0c;读者可通过以下命令查询。 configure --help1&…

[附源码]计算机毕业设计springboot小区物业管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

现金储备超400亿的小鹏,进入中途蓄力时刻

作者 | 刘洪 编辑 | Bruce造车新势力正在变得更加成熟。 11月30日美股盘前&#xff0c;小鹏汽车发布2022年Q3财报。如果说第一季度盈利曙光初现&#xff0c;第二季度逆势增长&#xff0c;那么第三季度&#xff0c;就是一次中途蓄力。 报告期内&#xff0c;小鹏的净亏损环比收窄…

Seata的这些安保机制是否会让你更放心

一、背景 SpringBoot 项目&#xff0c;通过引入seata-spring-boot-starter来接入 Seata&#xff0c;Seata 的能力会通过 SpringBoot 的自动装配机制来引入。在学习的时候是梳理有什么强大的、科技感十足的能力&#xff0c;但在试点的时候则更多考虑的是有哪些安保机制&#xf…

将内网网站发布上线【免服务器】

什么是cpolar&#xff1f; cpolar是一个非常强大的内网穿透工具&#xff0c;开发调试的必备利器。 它可以将本地内网服务器的HTTP、HTTPS、TCP协议端口映射为公网地址端口&#xff0c;使得公网用户可以轻松访问您的内网服务器&#xff0c;无需部署至公网服务器。支持永久免费使…

Mysql进阶学习(七)联合查询与DML语言

Mysql进阶学习&#xff08;七&#xff09;联合查询与DML语言进阶9&#xff1a;联合查询语法&#xff1a;特点&#xff1a;★案例DML语言1、插入语句1.1.插入的值的类型要与列的类型一致或兼容1.2.不可以为null的列必须插入值。可以为null的列如何插入值&#xff1f;1.3.列的顺序…

机械转码日记【26】二叉搜索树

目录 前言 1.二叉搜索数的概念 2.二叉搜索树的实现 2.1 基本架构 2.2二叉搜索树的插入 2.2.1普通版本 2.2.2递归版本 2.3二叉搜索树的查找 2.3.1普通版本 2.3.2递归版本 2.4二叉搜索树的删除 2.4.1普通版本代码 2.4.2递归版本代码 2.5搜索树的析构函数 2.6搜…

电脑黑屏按什么键恢复?只需要3个键就可以解决黑屏

今天和大家聊一聊电脑黑屏这个问题。相信大家都遇到过电脑黑屏&#xff0c;但是却不知道该如何解决&#xff0c;今天就来给大家分享一些处理方法。如果是电脑黑屏的话&#xff0c;一般情况下&#xff0c;只需要三个键就可以解决问题&#xff0c;电脑黑屏按什么键恢复&#xff1…

【Matplotlib绘制图像大全】(九):Matplotlib使用xticks()修改x轴刻度位置信息

前言 大家好,我是阿光。 本专栏整理了《Matplotlib绘制图像大全》,内包含了各种常见的绘图方法,以及Matplotlib各种内置函数的使用方法,帮助我们快速便捷的绘制出数据图像。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmMatp…