spring-security-oauth2-authorization-server实现用户名密码登录

一、版本

spring-security-oauth2-authorization-server:0.2.2
spring-boot:2.5.6

二、重要类

通过源码,发现授权码模式、客户端模式都实现或继承下面几个类

  • 1.OAuth2AuthorizationGrantAuthenticationToken

    用于存放 密码模式(password)所需的各种信息,包括 username、password、scopes 等

  • 2.AuthenticationConverter

    用于从HttpServletRequest中提取 密码模式(password) 所需的各种信息,包括username、password、scopes等

  • 3.AuthenticationProvider

    自定义 密码模式(password) 的核心逻辑,其功能主要如下:
    - 检验 密码模式(password) 所需信息的正确性,包括 username、password、scopes 等
    - 检验通过后,生成并返回 Access token、Refresh token、ID token 等信息

三、实现

  • 1、自定义OAuth2PasswordAuthenticationToken继承OAuth2AuthorizationGrantAuthenticationToken
/**
* 用于存放username与password
*/
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
    private static final long serialVersionUID = -559176897708927684L;
    private final String username;
    private final String password;


    public OAuth2PasswordAuthenticationToken(String username, String password, Authentication clientPrincipal, Map<String, Object> additionalParameters) {
        super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }
}
  • 2、自定义OAuth2PasswordAuthenticationConverter实现AuthenticationConverter
/**
* 从HttpServletRequest中提取username与password,传递给OAuth2PasswordAuthenticationToken
*/
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
    @Override
    public Authentication convert(HttpServletRequest request) {
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
            return null;
        }
        Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
        MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);

        String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
        if (!StringUtils.hasText(username) ||
                parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
            OAuth2EndpointUtils.throwError(
                    OAuth2ErrorCodes.INVALID_REQUEST,
                    OAuth2ParameterNames.USERNAME,"");
        }
        String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);

        Map<String, Object> additionalParameters = new HashMap<>();
        parameters.forEach((key, value) -> {
            if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
                    !key.equals(OAuth2ParameterNames.CLIENT_ID) &&
                    !key.equals(OAuth2ParameterNames.USERNAME) &&
                    !key.equals(OAuth2ParameterNames.PASSWORD)) {
                additionalParameters.put(key, value.get(0));
            }
        });

        return new OAuth2PasswordAuthenticationToken(username,password,clientPrincipal,additionalParameters);
    }
}
  • 3、自定义OAuth2PasswordAuthenticationProvider实现AuthenticationProvider
/**
* 密码认证的核心逻辑
*/
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {
    private static final StringKeyGenerator DEFAULT_REFRESH_TOKEN_GENERATOR =
            new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
    private final OAuth2AuthorizationService authorizationService;
    private final JwtEncoder jwtEncoder;
    private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
    private Supplier<String> refreshTokenGenerator = DEFAULT_REFRESH_TOKEN_GENERATOR::generateKey;
    private ProviderSettings providerSettings;

    public OAuth2PasswordAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder){
        Assert.notNull(authorizationService, "authorizationService cannot be null");
        Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
        this.authorizationService = authorizationService;
        this.jwtEncoder = jwtEncoder;
    }

    public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
        Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
        this.jwtCustomizer = jwtCustomizer;
    }

    public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
        Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
        this.refreshTokenGenerator = refreshTokenGenerator;
    }

    @Autowired
    protected void setProviderSettings(ProviderSettings providerSettings) {
        this.providerSettings = providerSettings;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2PasswordAuthenticationToken passwordAuthentication =
                (OAuth2PasswordAuthenticationToken) authentication;

        OAuth2ClientAuthenticationToken clientPrincipal =
                getAuthenticatedClientElseThrowInvalidClient(passwordAuthentication);
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();

        // 校验账户
        var username = passwordAuthentication.getUsername();
        if (StrUtil.isBlank(username)){
            throw new OAuth2AuthenticationException("账户不能为空");
        }
        // 校验密码
        var password = passwordAuthentication.getPassword();
        if (StrUtil.isBlank(password)){
            throw new OAuth2AuthenticationException("密码不能为空");
        }
        // 查询账户信息
        UserDetails userDetails = getUserDetails(username);
        if (ObjectUtil.isEmpty(userDetails)) {
            throw new OAuth2AuthenticationException("账户信息不存在,请联系管理员");
        }
        // 校验密码
        if (!PasswordEncoderFactories.createDelegatingPasswordEncoder().encode(password).equals(userDetails.getPassword())) {
            throw new OAuth2AuthenticationException("密码不正确");
        }

        // 构造认证信息
        Authentication principal = new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());

        //region 直接构造一个OAuth2Authorization对象,实际场景中,应该去数据库进行校验
        OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(principal.getName())
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .attribute(Principal.class.getName(), principal)
                .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, new HashSet<>())
                .build();
        //endregion


        String issuer = this.providerSettings != null ? this.providerSettings.getIssuer() : null;
        Set<String> authorizedScopes = authorization.getAttribute(
                OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
        // 构造jwt token信息
        JoseHeader.Builder headersBuilder = JwtUtils.headers();
        headersBuilder.header("client-id", registeredClient.getClientId());
        headersBuilder.header("authorization-grant-type", passwordAuthentication.getGrantType().getValue());
        JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(registeredClient, issuer, authorization.getPrincipalName(), authorizedScopes);

        // @formatter:off
        JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
                .registeredClient(registeredClient)
                .principal(authorization.getAttribute(Principal.class.getName()))
                .authorization(authorization)
                .authorizedScopes(authorizedScopes)
                .tokenType(OAuth2TokenType.ACCESS_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .authorizationGrant(passwordAuthentication)
                .build();
        // @formatter:on

        this.jwtCustomizer.customize(context);

        JoseHeader headers = context.getHeaders().build();
        JwtClaimsSet claims = context.getClaims().build();
        Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
        // 生成token
        OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
                jwtAccessToken.getExpiresAt(), authorizedScopes);
                
        return new OAuth2AccessTokenAuthenticationToken(
                registeredClient, clientPrincipal, accessToken);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {

        OAuth2ClientAuthenticationToken clientPrincipal = null;

        if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
            clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
        }

        if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
            return clientPrincipal;
        }

        throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
    }

    /**
     * 获取并组装用户信息
     */
    @SneakyThrows
    private UserDetails getUserDetails(String username) {
        return User.builder()
                    .username("用户名")
                    .password("密码")
                    .authorities("权限")
                    .build();
    }

四、重点

阅读源码发现,我们自定义的类不能被spring扫描到,能力和时间有限也没找到注入的接口,所以采取了比较原始方法(也是比较暴力的方法) 重写

  • 1、重写OAuth2TokenEndpointConfigurer

    方法HttpSecurityBuilder增加密码认证相关类

private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
        List<AuthenticationProvider> authenticationProviders = new ArrayList<>();

        JwtEncoder jwtEncoder = org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2ConfigurerUtils.getJwtEncoder(builder);
        OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2ConfigurerUtils.getJwtCustomizer(builder);

        OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
                new OAuth2AuthorizationCodeAuthenticationProvider(
                        org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2ConfigurerUtils.getAuthorizationService(builder),
                        jwtEncoder);
        // 添加密码认证处理逻辑
        OAuth2PasswordAuthenticationProvider passwordAuthenticationProvider = new OAuth2PasswordAuthenticationProvider(
                org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2ConfigurerUtils.getAuthorizationService(builder),
                        jwtEncoder);

        if (jwtCustomizer != null) {
            authorizationCodeAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
            passwordAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
        }
        authenticationProviders.add(authorizationCodeAuthenticationProvider);
        authenticationProviders.add(passwordAuthenticationProvider);

        OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
                new OAuth2RefreshTokenAuthenticationProvider(
                        org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2ConfigurerUtils.getAuthorizationService(builder),
                        jwtEncoder);
        if (jwtCustomizer != null) {
            refreshTokenAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
        }
        authenticationProviders.add(refreshTokenAuthenticationProvider);

        OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
                new OAuth2ClientCredentialsAuthenticationProvider(
                        org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2ConfigurerUtils.getAuthorizationService(builder),
                        jwtEncoder);
        if (jwtCustomizer != null) {
            clientCredentialsAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
        }
        authenticationProviders.add(clientCredentialsAuthenticationProvider);

        return authenticationProviders;
    }
  • 2、重写OAuth2TokenEndpointFilter

    OAuth2TokenEndpointFilter方法加载密码认证相关类

public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager, String tokenEndpointUri) {
        this.accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
        this.errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
        this.authenticationDetailsSource = new WebAuthenticationDetailsSource();
        this.authenticationSuccessHandler = this::sendAccessTokenResponse;
        this.authenticationFailureHandler = this::sendErrorResponse;
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty");
        this.authenticationManager = authenticationManager;
        this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
        this.authenticationConverter = new DelegatingAuthenticationConverter(
                Arrays.asList(
                        new OAuth2AuthorizationCodeAuthenticationConverter(),
                        new OAuth2RefreshTokenAuthenticationConverter(),
                        new OAuth2ClientCredentialsAuthenticationConverter(),
                        // 增加密码认证解析类
                        new OAuth2PasswordAuthenticationConverter(),
                        ));
    }

五、总结

参照该方法可以实现openId登录、验证码登录等

你可能感兴趣的:(java,auth2认证,spring,java)