[Java][SpringBoot][SpringSecuriy]关于代码编写的请教/探讨/求证

佬太强了,之前我也用过security,不过没这么复杂,在security的过滤器机制前添加了自定义的一个过滤器,然后重写了userdetailservice。现在小项目我都不用security了,感觉好笨重,要配置好多东西。最近接触了satoken觉得还挺不错的

2 个赞

感谢你的回复

但是你贴出的代码, 实际的作用意义在于鉴权验证

这个代码我也有, 这是和认证截然不同的逻辑

正如你所说, 认证可以有不同的入口进入, 但是验证必然是统一入口

比如我在项目中实现的方式如下:

@Component
@RequiredArgsConstructor
public class GlobalBearerTokenAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProperties jwtProperties;

    private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;

    private final AuthenticationEntryPoint authenticationEntryPoint;

    private final GlobalBearerAuthenticationConverter authenticationConverter;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            Optional<GlobalAuthenticationToken> authenticationOptional = Optional.ofNullable(
                    authenticationConverter.convert(request));

            if (authenticationOptional.isPresent()) {
                GlobalAuthenticationToken authentication = authenticationOptional.get();

                String token = authentication
                        .getPrincipal()
                        .toString();

                try {
                    JWTUtil.verify(token, jwtProperties.getSecretKeyByte());
                } catch (JWTException e) {
                    throw new BadCredentialsException("凭证验证失败");
                }

                JWT jwt = JWTUtil.parseToken(token);

                jwt.setKey(jwtProperties.getSecretKeyByte());

                UserWrap userWrap = jwt
                        .getPayloads()
                        .toBean(UserWrap.class);

                Assert.isTrue(StrUtil.equalsIgnoreCase(userWrap.getSub(), "token"),
                        () -> new BadCredentialsException("凭证来源不明"));

                Assert.isTrue(
                        jwt.validate(jwtProperties.getValidateLeeway()),
                        () -> new AccountExpiredException("凭证已过期"));

                String tokenCache = Optional
                        .ofNullable(CacheTools.get("authentication:token:" + userWrap.getAud()))
                        .orElseThrow(() -> new AccountExpiredException("凭证已失效"));

                Object jwtId = JWTUtil
                        .parseToken(tokenCache)
                        .getPayload(JWTPayload.JWT_ID);

                Assert.isTrue(
                        ObjectUtil.equal(jwt.getPayload(JWTPayload.JWT_ID), jwtId),
                        () -> new AccountExpiredException("凭证已过时")
                );

                authentication = GlobalAuthenticationToken.authenticated(userWrap, null, authenticationDetailsSource.buildDetails(request));

                SecurityContextHolder
                        .getContext()
                        .setAuthentication(authentication);

            }
        } catch (AuthenticationException e) {
            SecurityContextHolder.clearContext();
            authenticationEntryPoint.commence(request, response, e);
            return;
        }
        filterChain.doFilter(request, response);
    }

}

这个跟你贴的第一段代码的作用意义其实是一样的

而你说的converter

@Component
@RequiredArgsConstructor
public class GlobalBearerAuthenticationConverter implements AuthenticationConverter {

    private final WebSecurityProperties webSecurityProperties;

    private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;

    @Override
    public GlobalAuthenticationToken convert(HttpServletRequest request) {

        WebSecurityProperties.AuthenticationProperties properties = webSecurityProperties.getAuthentication();

        String header = ServletUtil.getHeaderIgnoreCase(request, properties.getHeader());

        header = StrUtil.blankToDefault(header, ServletUtil.getHeaderIgnoreCase(request, "access-token"));

        if (StrUtil.isBlank(header)) {
            return null;
        }

        header = StrUtil.addPrefixIfNot(header, "Bearer ");

        Assert.isTrue(ReUtil.isMatch(properties.getRegex(), header), webSecurityProperties::authenticationFailure);

        Map<String, String> groupName2Match = ReUtil.getAllGroupNames(authPattern(), header);

        GlobalAuthenticationToken authentication = groupName2Match
                .values()
                .stream()
                .map(GlobalAuthenticationToken::unauthenticated)
                .findFirst()
                .orElseThrow(webSecurityProperties::authenticationFailure);

        authentication.setDetails(authenticationDetailsSource.buildDetails(request));
        return authentication;
    }

    private Pattern authPattern() {
        return PatternPool.get(
                webSecurityProperties.getAuthentication().getRegex(),
                webSecurityProperties.getAuthentication().getRegexFlag()
        );
    }

}

这个也是有的, 你可以再看看, 验证和认证的逻辑还是有些区别的

+1

1 个赞

emm… 理解了, 你那个validate4Username其实在承担原本数据密码验证的工作,但是你放在了loadUsername之前去干了这件事儿, 但是这个实现会让后面其他人写的时候需要至少三件事, 理论上还缺一不可

  1. Filter

  2. Token

  3. Provider

  4. Filter目前看来就是干参数(Body/Query/Path)转换以及路径配置,那么可以考虑基于Token自动完成Filter的配置, 如果是我来 可能会考虑从注解上来简化这件事,项目移动的时候根据读到所有GlobalAuthenticationToken的实现,通过注解上声明的类型来自动选择要怎么从Request中读取参数, 好处这样后面的人其实只需要声明Token就好了,当然坏处是比较黑盒子了, 如果在读取参数出现更多的逻辑那么就维护麻烦了

  5. Provider 也可以集成到Token那里面去,由GlobalAuthenticationToken 提供一个抽象的方法validate4Username, 但是这样职责就转移了, 显得很奇怪, 不建议吧

tips: 主要看多少个人用这个来开发, 如果不多其实现在你的结构是最好的

另外 BasicAuthenticationFilter 这个就是在做认证,它就是把请求参数转换成UsernamePasswordAuthenticationToken, 然后最后交由UsernamePasswordAuthenticationFilter 完成最后的验证工作.

做用户认证流程没什么问题,关于鉴权可以去查询下@PreAuthorize的用法 并完善您的loadUserByUsername接口实现(ps:如果需要做接口鉴权的话)

1 个赞

你说的没错, Filter/Token/Provider, 这也是我文中内容提到的核心三要素, 缺一不可

如果要接入新的认证流程, 就必须得实现这三要素

至于你提到的BasicAuthenticationFilter

其实他不是在做认证流程, 而是在做验证流程

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
			if (authRequest == null) {
				this.logger.trace("Did not process authentication request since failed to find "
						+ "username and password in Basic Authorization header");
				chain.doFilter(request, response);
				return;
			}
			String username = authRequest.getName();
			this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
			if (authenticationIsRequired(username)) {
				Authentication authResult = this.authenticationManager.authenticate(authRequest);
				SecurityContextHolder.getContext().setAuthentication(authResult);
				if (this.logger.isDebugEnabled()) {
					this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
				}
				this.rememberMeServices.loginSuccess(request, response, authResult);
				onSuccessfulAuthentication(request, response, authResult);
			}
		}
		catch (AuthenticationException ex) {
			SecurityContextHolder.clearContext();
			this.logger.debug("Failed to process authentication request", ex);
			this.rememberMeServices.loginFail(request, response);
			onUnsuccessfulAuthentication(request, response, ex);
			if (this.ignoreFailure) {
				chain.doFilter(request, response);
			}
			else {
				this.authenticationEntryPoint.commence(request, response, ex);
			}
			return;
		}

		chain.doFilter(request, response);
	}

首先是

UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);

这行代码的本质其实就是你首回帖中说的

BasicAuthenticationConverter

	@Override
	public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
		String header = request.getHeader(HttpHeaders.AUTHORIZATION);
		if (header == null) {
			return null;
		}
		header = header.trim();
		if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
			return null;
		}
		if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
			throw new BadCredentialsException("Empty basic authentication token");
		}
		byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
		byte[] decoded = decode(base64Token);
		String token = new String(decoded, getCredentialsCharset(request));
		int delim = token.indexOf(":");
		if (delim == -1) {
			throw new BadCredentialsException("Invalid basic authentication token");
		}
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim),
				token.substring(delim + 1));
		result.setDetails(this.authenticationDetailsSource.buildDetails(request));
		return result;
	}

他的作用就是提取请求头中的Authorization

也就是我们常见的

Authorization: Basic xxxx

然后根据xxxx反向提取生成出Token

如果该请求头不存在, 则会继续进入下一个FilterChain中

如果请求头存在, 则提取出username

String username = authRequest.getName();
this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
if (authenticationIsRequired(username)) {
		Authentication authResult = this.authenticationManager.authenticate(authRequest);
		SecurityContextHolder.getContext().setAuthentication(authResult);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
		}
		this.rememberMeServices.loginSuccess(request, response, authResult);
		onSuccessfulAuthentication(request, response, authResult);
}

其中

Authentication authResult = this.authenticationManager.authenticate(authRequest);

这行代码本质上其实还是进入了Provider, 而且还得是支持UsernamePasswordAuthenticationToken的Provider

本质上就是走了AbstractUserDetailsAuthenticationProvider, 以及子实现DaoAuthenticationProvider

这一套流程其实就是Security提供的模板化的实现

验证通过后

SecurityContextHolder.getContext().setAuthentication(authResult);

这行代码就是设置鉴权上下文, 认定验证通过了
后续的
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);

都只是额外操作罢了

这个就是验证流程, 是指接收到来自客户端的请求, 通过请求携带的鉴权信息来验证, 判断客户端请求是否合法

所以我说这是验证流程, 不是认证流程

另外关于你说是简化这一块

其实我也觉得比较繁琐, 所以我最开始想的优化方向, 或者说我觉得优雅实现

首先是Filter, 路由的实现其实也可以通过动态配置来实现, 当然无论是nacos/数据库还是其他什么方式都无所谓, 至少这个类可以省掉

然后是Provider, 核心流程在于提取出username, 然后进行校验, 无论是密码校验,验证码校验还是其他appid相关的校验, 这一块没有什么其他好办法来优化

最后是Token, 也就是Authentication, 本质上就是鉴权的承载体, 比较多样化的东西, 所以我也不知道该怎么去简化或者抽象这个概念

你说的这个应该是用户权限的实现, 这个不是什么太大问题

1 个赞

收藏了,兄弟写的不错 :+1:

嗯嗯 , 你这么讲也没啥问题吧
因为我的想法就是用BasicAuthenticationFilter的模式来完成参数的提取 然后最后走UsernamePasswordAuthenticationFilter(我这里专指DaoProvider这个), 但是就跟我说的一样我过头看理解了,你的诉求其实走了不同的认证机制, 就像短信验证码这种, 所以你势必需要实现Provider.

所以我的观点是 理解你的做法了 但是换我来Filter这个要素会屏蔽掉

:no_mouth: 好几年不搞Java, 果然废了啊

用Security确实很笨重 , 而且不会减轻你的工作量 , 自己的项目怎么简单怎么写就行了 , Security大部分时候的作用只是给了开发者一种规范而已

[quote=“藏柏, post:1, topic:128512, username:notochen”]
用spring不用security就很不地道
[/quote],这种话就是瞎扯淡,适合当前业务规模的才是最要的,能实现需求才是正道

是不是不需要用一个统一的Global来处理?

  • provider就继承DaoAuthenticationProvider
  • token就继承UsernamePasswordAuthenticationToken
  • filter就继承OncePerRequestFilter

这样是不是更简单些。

你说得对, 适合才是最重要的

我贴头部声明了, 咱们不细探讨这个问题, 只是一个引咎而已

探讨这个没有意义

没有get到简单的点

我去,学到啦佬!

我用的satoken,简单

From 软件开发 to 开发调优

真的难得见到一篇说的很透彻的帖子,三个字:没毛病。不要怀疑,这个就是完整的鉴权流程,按照我的理解,Spring Security 的鉴权方式的确如此

因为前面楼层里面出现对于 鉴权授权 的混淆,所以我简单明确一下这两者的区别。

  • 鉴权(Authentication):对于用户身份的认定,确认当前登录的用户为系统内 存在/支持 的用户身份。所以无论是 账号密码/手机验证码/微信 等都是为了一件事,确认你是当前系统的用户,因此我更喜欢称它为 认证
  • 授权(Authorization):当前用户(已登录用户或者匿名用户)对于当前应用程序的授权规则,决定了你当前可以访问的资源信息。

明确上面两个概念,就可以得到一个很明确的结论,先鉴权,后授权。即使你没有任何登录操作,Spring Security 也会默认给你一个 匿名用户 (Anonymous)的身份,在你拥有一个身份以后根据身份的授权规则来确定你是否可以访问指定的资源。

所以主题中,楼主在做的是 鉴权,即通过不同的方式,确认用户身份。对于这个流程,楼主实现得相当完善了。

但是提一个建议,楼主提到 用spring不用security就很不地道,我很赞同。相应的,如果使用令牌进行鉴权授权流程,例如是 JWT/JWE 的话,我推荐楼主可以看看 Spring Seucirty OAuth Resource Server,即将你的应用也当成一个资源服务器,你的所有授权流程,都可以通过配置一个 JwtDecoder 和一个 JwtEncoder 来交给 Spring Security 来进行管理,并且在登录的时候使用他内置的 nimbusds 处理 JWT/JWE 的库即可,而不需要一个 GlobalBearerTokenAuthenticationFilter 这种东西。另外它也可以通过 Converter 的方式把你的角色/权限存到 SecurityContext 内,还支持 JWT 的扩展字段、有效期验证、Issuer 验证等等。对我来说我觉得是最优雅的实现,给我节约了不少时间,并且天然支持 MethodSecurity