[SpringCloud 마이크로서비스 프로젝트 practice-mall4cloud 프로젝트(3)]——mall4cloud-auth

현재 프로젝트 로그인에 사용되는 인증 및 승인 방법은 비교적 간단하며, 인증은 토큰 방식, 인증은 사용자 이름과 비밀번호 방식, 보안문자 인증코드 로그인과 결합되어 있습니다. 다음 소개에서는 OAuth2 인증 방법이 추가됩니다.

폼 의존성

먼저 인증 모듈의 관련 종속성을 살펴보겠습니다.
여기에 이미지 설명을 삽입하세요.
①: 공통된 데이터베이스 모듈, 주로 페이징 도구, mybatis 구성, 분산 ID 및 기타 데이터베이스 관련에 관한 것입니다. 콘텐츠 ③: 일부 인증 필터링 구성을 캡슐화합니다. 및 필터 구현< /span> ⑤: fegin이 호출하는 내부 API 인터페이스 ④: 인증코드 관련
②: 캐시: 주로 키, crud 도구, Redis 분산 잠금 등과 같은 Redis 관련 콘텐츠를 작동합니다.


나코스 구성

Nacos는 토큰 생성을 위해 다음과 같은 내용으로 구성됩니다.
여기에 이미지 설명을 삽입하세요.

토큰 인증

소개하다

토큰 인증은 주로 사용자의 신원을 확인하는 데 사용됩니다.
일반적으로 사용자는 인증을 위해 사용자 이름과 비밀번호를 제공하고 서버는 인증 후 클라이언트에 액세스 토큰(Token)을 발급합니다. 클라이언트는 이 토큰을 사용하여 사용자 이름과 비밀번호를 다시 제공하지 않고도 후속 요청에서 자신의 신원을 증명할 수 있습니다.
토큰은 일반적으로 사용자 정보와 권한 정보를 포함할 수 있는 문자열입니다.

프로젝트 코드

@PostMapping("/ua/login")
	@Operation(summary = "账号密码" , description = "通过账号登录,还要携带用户的类型,也就是用户所在的系统")
	public ServerResponseEntity<TokenInfoVO> login(
			@Valid @RequestBody AuthenticationDTO authenticationDTO) {
    
    

		// 这边获取了用户的用户信息,那么根据sessionid对应一个user的原则,我应该要把这个东西存起来,然后校验,那么存到哪里呢?
		// redis,redis有天然的自动过期的机制,有key value的形式
		ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenResponse = authAccountService
				.getUserInfoInTokenByInputUserNameAndPassword(authenticationDTO.getPrincipal(),
						authenticationDTO.getCredentials(), authenticationDTO.getSysType());


		if (!userInfoInTokenResponse.isSuccess()) {
    
    
			return ServerResponseEntity.transform(userInfoInTokenResponse);
		}

		UserInfoInTokenBO data = userInfoInTokenResponse.getData();

		ClearUserPermissionsCacheDTO clearUserPermissionsCacheDTO = new ClearUserPermissionsCacheDTO();
		clearUserPermissionsCacheDTO.setSysType(data.getSysType());
		clearUserPermissionsCacheDTO.setUserId(data.getUserId());
		// 将以前的权限清理了,以免权限有缓存
		ServerResponseEntity<Void> clearResponseEntity = permissionFeignClient.clearUserPermissionsCache(clearUserPermissionsCacheDTO);

		if (!clearResponseEntity.isSuccess()) {
    
    
			return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
		}

		// 保存token,返回token数据给前端,这里是最重要的
		return ServerResponseEntity.success(tokenStore.storeAndGetVo(data));
	}

로그인을 완료한 후 토큰 토큰을 획득하여 프런트엔드에 반환합니다. 사용자 이름과 비밀번호 확인에 대해 다시 살펴보겠습니다.

if (StrUtil.isBlank(inputUserName)) {
    
    
			return ServerResponseEntity.showFailMsg("用户名不能为空");
		}
		if (StrUtil.isBlank(password)) {
    
    
			return ServerResponseEntity.showFailMsg("密码不能为空");
		}

		InputUserNameEnum inputUserNameEnum = null;

		// 用户名
		if (PrincipalUtil.isUserName(inputUserName)) {
    
    
			inputUserNameEnum = InputUserNameEnum.USERNAME;
		}

		if (inputUserNameEnum == null) {
    
    
			return ServerResponseEntity.showFailMsg("请输入正确的用户名");
		}

		AuthAccountInVerifyBO authAccountInVerifyBO = authAccountMapper
				.getAuthAccountInVerifyByInputUserName(inputUserNameEnum.value(), inputUserName, sysType);

		if (authAccountInVerifyBO == null) {
    
    
			prepareTimingAttackProtection();
			// 再次进行运算,防止计时攻击
			// 计时攻击(Timing
			// attack),通过设备运算的用时来推断出所使用的运算操作,或者通过对比运算的时间推定数据位于哪个存储设备,或者利用通信的时间差进行数据窃取。
			mitigateAgainstTimingAttack(password);
			return ServerResponseEntity.showFailMsg("用户名或密码不正确");
		}

		if (Objects.equals(authAccountInVerifyBO.getStatus(), AuthAccountStatusEnum.DISABLE.value())) {
    
    
			return ServerResponseEntity.showFailMsg("用户已禁用,请联系客服");
		}

		if (!passwordEncoder.matches(password, authAccountInVerifyBO.getPassword())) {
    
    
			return ServerResponseEntity.showFailMsg("用户名或密码不正确");
		}
		return ServerResponseEntity.success(BeanUtil.map(authAccountInVerifyBO, UserInfoInTokenBO.class));

사용자 이름을 통해 사용자 정보를 얻고, PasswordEncoder.matches()를 통해 비밀번호를 확인하고, 비밀번호에 성공하면 userInfoInTokenResponse.isSuccess() 성공 상태를 반환합니다. 아래로 내려가서permissionFeignClient를 호출하여 권한을 삭제하세요. 토큰을 얻는 최종 코드는 다음과 같습니다.

public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) {
    
    
		TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInToken);

		TokenInfoVO tokenInfoVO = new TokenInfoVO();
		tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
		tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
		tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn());
		return tokenInfoVO;
	}

/**
	 * 将用户的部分信息存储在token中,并返回token信息
	 * @param userInfoInToken 用户在token中的信息
	 * @return token信息
	 */
	public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInToken) {
    
    
		TokenInfoBO tokenInfoBO = new TokenInfoBO();
		String accessToken = IdUtil.simpleUUID();
		String refreshToken = IdUtil.simpleUUID();

		tokenInfoBO.setUserInfoInToken(userInfoInToken);
		tokenInfoBO.setExpiresIn(getExpiresIn(userInfoInToken.getSysType()));

		String uidToAccessKeyStr = getUidToAccessKey(getApprovalKey(userInfoInToken));
		String accessKeyStr = getAccessKey(accessToken);
		String refreshToAccessKeyStr = getRefreshToAccessKey(refreshToken);

		// 一个用户会登陆很多次,每次登陆的token都会存在 uid_to_access里面
		// 但是每次保存都会更新这个key的时间,而key里面的token有可能会过期,过期就要移除掉
		List<String> existsAccessTokens = new ArrayList<>();
		// 新的token数据
		existsAccessTokens.add(accessToken + StrUtil.COLON + refreshToken);

		Long size = redisTemplate.opsForSet().size(uidToAccessKeyStr);
		if (size != null && size != 0) {
    
    
			List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr, size);
			if (tokenInfoBoList != null) {
    
    
				for (String accessTokenWithRefreshToken : tokenInfoBoList) {
    
    
					String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
					String accessTokenData = accessTokenWithRefreshTokenArr[0];
					if (BooleanUtil.isTrue(stringRedisTemplate.hasKey(getAccessKey(accessTokenData)))) {
    
    
						existsAccessTokens.add(accessTokenWithRefreshToken);
					}
				}
			}
		}

		redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    
    

			long expiresIn = tokenInfoBO.getExpiresIn();

			byte[] uidKey = uidToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
			byte[] refreshKey = refreshToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
			byte[] accessKey = accessKeyStr.getBytes(StandardCharsets.UTF_8);

			for (String existsAccessToken : existsAccessTokens) {
    
    
				connection.sAdd(uidKey, existsAccessToken.getBytes(StandardCharsets.UTF_8));
			}

			// 通过uid + sysType 保存access_token,当需要禁用用户的时候,可以根据uid + sysType 禁用用户
			connection.expire(uidKey, expiresIn);

			// 通过refresh_token获取用户的access_token从而刷新token
			connection.setEx(refreshKey, expiresIn, accessToken.getBytes(StandardCharsets.UTF_8));

			// 通过access_token保存用户的租户id,用户id,uid
			connection.setEx(accessKey, expiresIn, Objects.requireNonNull(redisSerializer.serialize(userInfoInToken)));

			return null;
		});

		// 返回给前端是加密的token
		tokenInfoBO.setAccessToken(encryptToken(accessToken,userInfoInToken.getSysType()));
		tokenInfoBO.setRefreshToken(encryptToken(refreshToken,userInfoInToken.getSysType()));

		return tokenInfoBO;
	}

위 토큰 저장 코드에 대한 설명:

1. TokenInfoBO 객체 생성: 먼저 토큰 관련 정보를 저장하는 데 사용되는 TokenInfoBO 객체를 생성합니다.
2. 액세스 토큰 및 새로 고침 토큰 생성: IdUtil.simpleUUID()를 사용하여 임의의 액세스 토큰 및 새로 고침 토큰을 생성합니다.
3. 토큰 정보 설정: userInfoInToken 객체를 tokenInfoBO로 설정하고 토큰 만료 시간(expiresIn)을 설정합니다. 만료 시간은 userInfoInToken의 sysType에 따라 결정됩니다.
4. 관련 키 획득: uidToAccessKeyStr, accessKeyStr, RefreshToAccessKeyStr 등 토큰과 관련된 일부 키 값(Key)을 획득합니다.
기존 토큰 처리: Redis에서 데이터를 쿼리하여 동일한 사용자의 토큰이 이미 존재하는지 확인합니다. 있는 경우 새로 생성된 액세스 토큰 및 새로 고침 토큰을 기존 토큰 목록에 추가합니다.
5. Redis 파이프라이닝을 사용하여 데이터 저장: Redis의 파이프라이닝 메커니즘을 사용하여 한 번에 여러 Redis 명령을 실행하고 Redis에 토큰 및 관련 정보를 저장합니다. 이러한 명령에는 액세스 토큰 및 새로 고침 토큰을 사용자와 연결하고, 만료 시간을 설정하고, 사용자 정보를 저장하는 작업이 포함됩니다.
6. 암호화 토큰: encryptToken 메서드를 사용하여 액세스 토큰과 새로 고침 토큰을 암호화한 다음 암호화된 토큰을 tokenInfoBO로 설정합니다.
토큰 정보 반환: 마지막으로 프런트엔드 사용을 위한 액세스 토큰 및 새로 고침 토큰 정보가 포함된 tokenInfoBO 개체를 반환합니다.

공격 방지를 위한 후속 토큰 복호화와 토큰 갱신 코드는 코드 주석을 통해 관련 로직을 확인할 수 있으므로 하나씩 설명하지는 않겠습니다.

필터 점검

프런트 엔드가 토큰을 획득한 후 이 정보를 액세스 인터페이스에 전달하고 백엔드 서비스는 액세스된 인터페이스가 인증을 통과할 수 있는지 확인하기 위해 필터를 통해 이를 확인합니다. 주요 코드는 이 패키지 아래에 있습니다.
여기에 이미지 설명을 삽입하세요.

@Component
public class AuthFilter implements Filter {
    
    

	private static Logger logger = LoggerFactory.getLogger(AuthFilter.class);

	@Autowired
	private AuthConfigAdapter authConfigAdapter;

	@Autowired
	private HttpHandler httpHandler;

	@Autowired
	private TokenFeignClient tokenFeignClient;

	@Autowired
	private PermissionFeignClient permissionFeignClient;

	@Autowired
	private FeignInsideAuthConfig feignInsideAuthConfig;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    
    
		HttpServletRequest req = (HttpServletRequest) request;
		HttpServletResponse resp = (HttpServletResponse) response;

		if (!feignRequestCheck(req)) {
    
    
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		if (Auth.CHECK_TOKEN_URI.equals(req.getRequestURI())) {
    
    
			chain.doFilter(req, resp);
			return;
		}


		List<String> excludePathPatterns = authConfigAdapter.excludePathPatterns();

		// 如果匹配不需要授权的路径,就不需要校验是否需要授权
		if (CollectionUtil.isNotEmpty(excludePathPatterns)) {
    
    
			for (String excludePathPattern : excludePathPatterns) {
    
    
				AntPathMatcher pathMatcher = new AntPathMatcher();
				if (pathMatcher.match(excludePathPattern, req.getRequestURI())) {
    
    
					chain.doFilter(req, resp);
					return;
				}
			}
		}

		String accessToken = req.getHeader("Authorization");

		if (StrUtil.isBlank(accessToken)) {
    
    
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		// 校验token,并返回用户信息
		ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenVoServerResponseEntity = tokenFeignClient
				.checkToken(accessToken);
		if (!userInfoInTokenVoServerResponseEntity.isSuccess()) {
    
    
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		UserInfoInTokenBO userInfoInToken = userInfoInTokenVoServerResponseEntity.getData();

		// 需要用户角色权限,就去根据用户角色权限判断是否
		if (!checkRbac(userInfoInToken,req.getRequestURI(), req.getMethod())) {
    
    
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		try {
    
    
			// 保存上下文
			AuthUserContext.set(userInfoInToken);

			chain.doFilter(req, resp);
		}
		finally {
    
    
			AuthUserContext.clean();
		}

	}

	private boolean feignRequestCheck(HttpServletRequest req) {
    
    
		// 不是feign请求,不用校验
		if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_INSIDE_URL_PREFIX)) {
    
    
			return true;
		}
		String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey());

		// 校验feign 请求携带的key 和 value是否正确
		if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret,feignInsideAuthConfig.getSecret())) {
    
    
			return false;
		}
		// ip白名单
		List<String> ips = feignInsideAuthConfig.getIps();
		// 移除无用的空ip
		ips.removeIf(StrUtil::isBlank);
		// 有ip白名单,且ip不在白名单内,校验失败
		if (CollectionUtil.isNotEmpty(ips)
				&& !ips.contains(IpHelper.getIpAddr())) {
    
    
			logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr());
			return false;
		}
		return true;
	}

	/**
	 * 用户角色权限校验
	 * @param uri uri
	 * @return 是否校验成功
	 */
	public boolean checkRbac(UserInfoInTokenBO userInfoInToken, String uri, String method) {
    
    

		if (!Objects.equals(SysTypeEnum.PLATFORM.value(), userInfoInToken.getSysType()) && !Objects.equals(SysTypeEnum.MULTISHOP.value(), userInfoInToken.getSysType())) {
    
    
			return true;
		}

		ServerResponseEntity<Boolean> booleanServerResponseEntity = permissionFeignClient
				.checkPermission(userInfoInToken.getUserId(), userInfoInToken.getSysType(),uri,userInfoInToken.getIsAdmin(),HttpMethodEnum.valueOf(method.toUpperCase()).value() );

		if (!booleanServerResponseEntity.isSuccess()) {
    
    
			return false;
		}

		return booleanServerResponseEntity.getData();
	}

}

필터링 논리는 아래에 자세히 설명되어 있습니다.
여기에 이미지 설명을 삽입하세요.
①:

이 코드의 doFilter 메소드는 Filter 인터페이스를 구현하는 메소드로, HTTP 요청의 필터링 로직을 처리하는 데 사용됩니다. 콜백 메소드로, 요청이 도착하면 컨테이너는 이 메소드를 호출하여 일부 전처리 및 후처리 작업을 수행합니다.
메소드의 매개변수는 다음과 같습니다.
요청: 요청된 정보와 데이터를 얻는 데 사용할 수 있는 HTTP 요청 개체(일반적으로 ServletRequest 유형)를 나타냅니다.
응답: 일반적으로 응답 데이터를 생성하고 보내는 데 사용되는 ServletResponse 유형의 HTTP 응답 개체를 나타냅니다.
체인: 요청 처리를 계속하거나 요청을 다음 필터로 전달하는 데 사용할 수 있는 필터 체인(FilterChain)을 나타냅니다.

③: 내부요청 확인

private boolean feignRequestCheck(HttpServletRequest req) {
    
    
		// 不是feign请求,返回true
		if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_INSIDE_URL_PREFIX)) {
    
    
			return true;
		}
		//获取fegin的value密钥,这个在nacos中已配置
		String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey());

		// 校验feign 请求携带的key 和 value是否正确,不正确返回false
		if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret,feignInsideAuthConfig.getSecret())) {
    
    
			return false;
		}
		// ip白名单
		List<String> ips = feignInsideAuthConfig.getIps();
		// 移除无用的空ip
		ips.removeIf(StrUtil::isBlank);
		// 有ip白名单,且ip不在白名单内,校验失败。不为空或者获取当前用户真实ip不在白名单中,返回false
		if (CollectionUtil.isNotEmpty(ips)
				&& !ips.contains(IpHelper.getIpAddr())) {
    
    
			logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr());
			return false;
		}
		return true;
	}

위의 내용이 false를 반환하면 fegin 요청이지만 통과하지 못하면 판단에 포함된 코드가 통과됩니다. 이는 전송된 응답이 "승인되지 않음"임을 의미합니다. fegin 요청 확인이 실패하면 실패로 간주됩니다.

if (!feignRequestCheck(req)) {
    
    
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

④: 토큰에 대한 요청이 검증되면 이 필터는 무시되고 다음 필터로 전달되는데, 이것이 chain.doFilter(request, response)의 역할이기도 합니다. 그러나 현재 시스템 필터 체인에는 필터가 하나만 있음을 알 수 있습니다. AuthConfig에서.
여기에 이미지 설명을 삽입하세요.

@ConditionalOnMissingBean은 Spring 프레임워크 애플리케이션, 특히 Spring Boot에서 일반적으로 사용되는 주석으로, 동일한 유형의 다른 Bean이 애플리케이션 컨텍스트에 이미 존재하는지 여부에 따라 조건부로 Bean을 구성하는 데 사용됩니다. 이 주석은 Spring의 주석 기반 구성의 일부이며 Bean의 인스턴스화를 제어하는 ​​데 사용됩니다.

⑤: 현재 이 목록에는 제외해야 할 다음 URL이 있으며 이는 빈 주입을 통해 달성됩니다.
표시된 것처럼 여전히 이 구성 코드, 첫 번째 빈입니다. 아래 스크린샷에서 제외해야 할 것은 무엇입니까?

@Configuration
public class AuthConfig {
    
    

	@Bean
	@ConditionalOnMissingBean
	public AuthConfigAdapter authConfigAdapter() {
    
    
		return new DefaultAuthConfigAdapter();
	}

	@Bean
	@Lazy
	public FilterRegistrationBean<AuthFilter> filterRegistration(AuthConfigAdapter authConfigAdapter, AuthFilter authFilter) {
    
    
		FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>();
		// 添加过滤器
		registration.setFilter(authFilter);
		// 设置过滤路径,/*所有路径
		registration.addUrlPatterns(ArrayUtil.toArray(authConfigAdapter.pathPatterns(), String.class));
		registration.setName("authFilter");
		// 设置优先级
		registration.setOrder(0);
		registration.setDispatcherTypes(DispatcherType.REQUEST);
		return registration;
	}

}

여기에 이미지 설명을 삽입하세요.
여기에 이미지 설명을 삽입하세요.
⑥그런 다음 이 목록을 반복하여 정기적으로 일치시킵니다. 이 인증 필터를 통과할 필요가 없는 URL을 제외합니다.
78: 정식 요청입니다. Authorization의 내용이 포함되어 있는지 확인하고, 비어 있으면 바로 Unauthorized를 반환합니다.
다음 코드는 다음과 같습니다
여기에 이미지 설명을 삽입하세요.
① 인터페이스 코드는 다음과 같습니다. OpenFegin 인터페이스를 호출합니다. 프로젝트에서 fegin을 어떻게 구현하는지 살펴보겠습니다.
먼저, mall4cloud-api 패키지 하위에 특정 모듈에서 제공할 api 인터페이스가 구축되어 있는데, fegin 패키지 아래에서 인터페이스 중 하나를 선택하여 조회합니다

@FeignClient(value = “mall4cloud-auth”, contextId = “token”) 주석에 의해 수정된 것을 볼 수 있습니다. value는 서비스 이름을 지정하고 contextId는 이 인터페이스의 고유 식별자를 지정합니다. @GetMapping(value = Auth.CHECK_TOKEN_URI)은 액세스된 URI를 지정합니다.
여기에 이미지 설명을 삽입하세요.

그런 다음 인터페이스 구현이옵니다.

은 각 모듈의 fegin 패키지 하위에 구현되며, @RestController Annotation을 통해 구현됩니다. 이 접근 방식은 API 구현 클래스를 수정하는 @DubboService 주석과 유사합니다.
여기에 이미지 설명을 삽입하세요.
'''''''
여기에 이미지 설명을 삽입하세요.
일반적인 feign 인터페이스는 http 요청을 통해 직접 호출되는데, checkToken() 메서드가 호출되면 실제로 호출됩니다. Feign에 의해 생성된 프록시 개체의 메서드입니다.
프록시 객체는 메소드 정의 및 주석(예: @GetMapping(value = Auth.CHECK_TOKEN_URI))을 기반으로 HTTP 요청을 생성하고 해당 요청을 원격 서비스의 해당 경로로 보냅니다.
원격 서비스가 응답한 후 프록시 객체는 응답을 ServerResponseEntity< UserInfoInTokenBO > 유형으로 구문 분석하여 반환합니다.
그런데 mall4cloud-auth 모듈 코드에는 "/feign/token/checkToken" 요청 URI와 해당 컨트롤러가 없습니다.
동시에 "/ feign/token/checkToken" 요청도 위에서 설명한 대로 인터셉터에 의해 허용됩니다.

23권한 확인
가맹점 측, 플랫폼 측, 사용자 측으로 나누어지며, 이후 특정 URI에 대한 권한이 있는지 확인합니다. 주요 코드는 다음과 같습니다. 이번 권한 확인은 다음과 같습니다. rbac 모듈에 속하며 다음 장에서 자세히 소개하겠습니다.
여기에 이미지 설명을 삽입하세요.
검증에 실패하면 승인되지 않은 상태 코드가 반환됩니다.
여기에 이미지 설명을 삽입하세요.
④: 사용ThreadLocal 사용자 정보를 저장합니다. 스레드 간 격리를 달성하고 스레드 전체에 컨텍스트 정보를 전송할 수 있습니다.

스레드로부터 안전한 데이터 격리: ThreadLocal을 사용하면 멀티 스레드 환경에서 데이터를 격리하여 각 스레드가 자체적인 독립적인 데이터 복사본을 갖도록 보장함으로써 여러 스레드 간의 데이터 공유 및 경합 조건을 방지할 수 있습니다. 이는 사용자 로그인 정보, 세션 정보 등과 같은 일부 상황별 데이터에 매우 유용합니다.
컨텍스트 정보 전달: 모든 메소드 호출에서 매개변수로 명시적으로 전달하지 않고도 전체 스레드 컨텍스트에 걸쳐 특정 정보를 전달해야 하는 상황이 있을 수 있습니다. ThreadLocal을 사용하면 이러한 상황별 정보를 저장하고 액세스할 수 있습니다.

그런 다음 다음 필터(확장 가능)를 입력하고 사용자 정보를 모두 사용한 후 해제합니다.

요약하다

인증 모듈의 코드 로직의 주요 프로세스는 로그인 인터페이스 /ua/login->tokenStore 아래의 storeAccessToken() 메소드이며, 토큰 저장이 완료된 후 필터 AuthFilter 클래스의 구현입니다. doFilter() 메소드는 액세스 경로(요청) 권한 부여 및 사용자 역할 권한 부여를 수행합니다(여기에서는 주로 rbac 모듈의 서비스가 호출됩니다).

이 인증 모듈의 코드는 주류 사용자 인증 및 권한 부여 기능이 아닙니다. 모놀리식 아키텍처 서비스에 더 가깝습니다. 문의사항이 있는 부분은 다음과 같습니다.

  1. 첫 번째는 uuid 메소드를 직접 사용하는 토큰 생성입니다. 이렇게 하면 예측 가능하며 정보를 저장하지 않고 임의의 문자열로 처리됩니다. 물론 사용자 정보를 캐시하기 위해 분산 Redis를 사용하는 것이 합리적입니다.

위 방법을 이용하면 로그인 시 랜덤코드를 획득한 후, 랜덤코드를 암호화하여 토큰을 생성할 수 있습니다
토큰이 사용자 정보를 저장하거나 사용자 권한을 높이면 jwt 방식을 사용할 수 있습니다. 이것은 또한 배포 방법이기도 합니다

  1. 인증에 사용되는 필터는 서블릿 아래의 필터이며, 대부분의 논리적 작업을 수동으로 작성하고 지정해야 하며 SpringSecrity 또는 Shiro와 같은 일부 널리 사용되는 권한 인증 프레임워크는 사용되지 않습니다. 학습 프로젝트에서는 권한 부여 및 인증 로직을 배울 수 있지만 엔터프라이즈 수준 프로젝트에서는 인증 및 권한 부여가 충분히 정제되지 않습니다. 그러나 전반적인 논리는 일관됩니다.

예를 들어, 필터의 일부 릴리스 인터페이스는 구성을 기반으로 하지 않고 코드에 하드 코딩되어 있습니다. 또는 인터페이스 액세스의 경우 승인되지 않은 경우 일반적으로 승인되지 않은 상태로 직접 돌아가는 대신 지정된 로그인 페이지로 점프하여 프롬프트를 표시해야 합니다. 이는 SpringSecrity 프레임워크를 통해 더욱 정교하게 수행될 수 있습니다.

사용자 인증 및 권한 부여를 위해 SpringSecrity 또는 shiro 프레임워크 프로젝트를 사용하는 방법에 대한 기사는 나중에 추가될 예정입니다.

Acho que você gosta

Origin blog.csdn.net/qq_40454136/article/details/132861108
Recomendado
Clasificación