Spring Authorization Server扩展密码模式

新项目都开始用JDK17了,Springboot也用3.x了,对应的认证授权服务也要升级了。Spring OAuth2.0不再支持了,开始使用Spring Authorization Server了。因为去掉了密码模式,而我们项目最适用的还是密码模式,这里我们使用OAuth2.1的扩展功能补上密码模式。
参考官方文档,给出了步骤:

image.png

此次使用Springboot版本为:3.3.3
依赖引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.32</version>
</dependency>

1. 密码模式

按照文档,添加密码模式,主要添加几个文件:

    1. Converter类:主要是限定请求Token的参数的,并未验证参数的值,验证好参数后,会将参数封装成passwordToken实体,往下传
    1. Provider类,主要作用2个,1是验证参数值,2是用这些数据生成Token。数据来源就是Converter类传过来的passwordToken实体
    1. passwordToken实体:实体类功能,有一些必须得属性
    1. 密码模式用到的工具类util

以上内容都是参考授权码模式或是客户端模式来写的。

1.1 Converter类 - OAuth2PasswordAuthenticationConverter
/**
 * 限定必须要传的参数
 * 只是要这些参数,并没有对参数值校验
 */
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {

    @Nullable
    @Override
    public Authentication convert(HttpServletRequest request) {
        // 1. 提取表单参数,准备校验用,用的这个方法就是复制的sas自带的
        MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
        // 2. 授权类型参数 (必须)
//      String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
        if (!OAuth2PasswordAuthenticationToken.PASSWORD.getValue().equals(grantType)) {
            return null;
        }
        // 3. 令牌申请访问范围参数 (可选)
        String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
        if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
            OAuth2EndpointUtils.throwError(
                    OAuth2ErrorCodes.INVALID_REQUEST,
                    OAuth2ParameterNames.SCOPE,
                    OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
        }

        Set<String> requestedScopes = null;
        if (StringUtils.hasText(scope)) {
            requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
        }
        // 4. 账号密码参数校验
        // 4.1 用户名参数 (必须)
        String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
        if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
            OAuth2EndpointUtils.throwError(
                    OAuth2ErrorCodes.INVALID_REQUEST,
                    OAuth2ParameterNames.USERNAME,
                    OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
        }

        // 4.2 密码参数 (必须)
        String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
        if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
            OAuth2EndpointUtils.throwError(
                    OAuth2ErrorCodes.INVALID_REQUEST,
                    OAuth2ParameterNames.PASSWORD,
                    OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
        }

        // 5. 客户端凭据信息,在header 中填写的那个
        Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();

        // 6. 参数封装成additionalParameters放到OAuth2PasswordAuthenticationToken中传给 PasswordAuthenticationProvider 用于验证值
        Map<String, Object> additionalParameters = new HashMap<>();
        parameters.forEach((key, value) -> {
            if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && !key.equals(OAuth2ParameterNames.SCOPE)) {
                additionalParameters.put(key, value.get(0));
            }
        });
        return new OAuth2PasswordAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters);
    }

}
1.2 Provider类 - OAuth2PasswordAuthenticationProvider
/**
 * 参考授权码(主要)"OAuth2AuthorizationCodeAuthenticationProvider"和客户端"OAuth2ClientCredentialsAuthenticationProvider"
 */
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {
    
    @Resource
    private PasswordEncoder passwordEncoder;
    
    // 这部分代码和OAuth2ClientCredentialsAuthenticationProvider类似,只是添加了AuthenticationManager
    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";

    private final AuthenticationManager authenticationManager;
    private final OAuth2AuthorizationService authorizationService;
    private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
    
    
    public OAuth2PasswordAuthenticationProvider(AuthenticationManager authenticationManager, 
                                OAuth2AuthorizationService authorizationService,
                                OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {

        Assert.notNull(authorizationService, "authorizationService cannot be null");
        Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
        this.authenticationManager = authenticationManager;
        this.authorizationService = authorizationService;
        this.tokenGenerator = tokenGenerator;
    }
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        
//      PasswordAuthenticationToken
        OAuth2PasswordAuthenticationToken passwordAuthenticationToken = (OAuth2PasswordAuthenticationToken) authentication;
        
        // coverter中最后生成的token,包含3部分内容,在这里拿出来,下面用
        Map<String, Object> additionalParameters = passwordAuthenticationToken.getAdditionalParameters();
        OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient(passwordAuthenticationToken);
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
        
        // 1. 验证客户端是否支持密码模式类型(grant_type=password)
        if (!registeredClient.getAuthorizationGrantTypes().contains(passwordAuthenticationToken.getGrantType())) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE);
        }
        // 2. 校验范围scope
        Set<String> authorizedScopes = registeredClient.getScopes();
        Set<String> requestedScopes  = passwordAuthenticationToken.getScopes();
        if (!CollectionUtils.isEmpty(requestedScopes )) {
            Set<String> unauthorizedScopes = requestedScopes.stream()
                    .filter(scope -> !registeredClient.getScopes().contains(scope))
                    .collect(Collectors.toSet());
            if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
                throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
            }
            authorizedScopes = new LinkedHashSet<>(requestedScopes);
        }
        // 3 用户名密码校验
        String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
        String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
        // 我们不自己校验了,用oauth2的方法校验
//        MyUserDetail userDetail = userDetailsService.loadUserByUsername(username);
//        if (userDetail == null) {
//          throw new OAuth2AuthenticationException("用户不存在!");
//      }
//        if (!passwordEncoder.matches(password, userDetail.getPassword())) {
//            throw new OAuth2AuthenticationException("密码不正确!");
//        }
       
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        Authentication usernamePasswordAuthentication = null;
        try {
            usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        } catch (AuthenticationException e) {
            e.printStackTrace();
            throw new OAuth2AuthenticationException("账号或密码错误");
        }
        // 处理userDetails的时候,我们没有添加权限信息,这拿不到数据
//        Collection<? extends GrantedAuthority> authorities = usernamePasswordAuthentication.getAuthorities();
//        for (GrantedAuthority authoriti : authorities) {
//      }
        
        // 4. 生成token
        // 4.1 填充token需要的上下文数据,按照授权码模式来的
        Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
                .registeredClient(registeredClient)
                // 身份验证成功的认证信息(用户名、权限等信息)
                .principal(usernamePasswordAuthentication) 
                .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                .authorizedScopes(authorizedScopes)
                // 授权类型
                .authorizationGrantType(passwordAuthenticationToken.getGrantType())
                // 授权具体对象
                .authorizationGrant(passwordAuthenticationToken)
                ;
        // 4.2 生成访问令牌(Access Token)
        DefaultOAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
        OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
        if (generatedAccessToken == null) {
            OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                    "The token generator failed to generate the access token.", ERROR_URI);
            throw new OAuth2AuthenticationException(error);
        }
        OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
                generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());

        // 4. 生成刷新令牌(Refresh Token)
        OAuth2RefreshToken refreshToken = null;
        if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
                // Do not issue refresh token to public client
                !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
            tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
            OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
            if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
                OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                        "The token generator failed to generate the refresh token.", ERROR_URI);
                throw new OAuth2AuthenticationException(error);
            }
            refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
        }
        
        
        // 5. 组装数据入库
        OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(clientPrincipal.getName())
                .authorizationGrantType(passwordAuthenticationToken.getGrantType())
                .authorizedScopes(authorizedScopes)
                .attribute(Principal.class.getName(), usernamePasswordAuthentication);
        if (generatedAccessToken instanceof ClaimAccessor) {
            authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
        } else {
            authorizationBuilder.accessToken(accessToken);
            authorizationBuilder.refreshToken(refreshToken);
        }
        OAuth2Authorization authorization = authorizationBuilder.build();
        // 入库
        this.authorizationService.save(authorization);
        additionalParameters = Collections.emptyMap();

        return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
1.3 passwordToken实体 - OAuth2PasswordAuthenticationToken
/**
 * 这个token就是传递数据用的一个实体,想要什么数据可以写成变量
 * 在converter生成时赋值,在provider中拿出来用
 */
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
    
    private static final long serialVersionUID = -7029686994815546552L;

    public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
    
    private final Set<String> scopes;

    /**
     * 密码模式身份验证令牌
     *
     * @param clientPrincipal      客户端信息
     * @param scopes               令牌申请访问范围
     * @param additionalParameters 自定义额外参数(用户名和密码等)
     */
    protected OAuth2PasswordAuthenticationToken(Authentication clientPrincipal, @Nullable Set<String> scopes,
            @Nullable Map<String, Object> additionalParameters) {
        super(PASSWORD, clientPrincipal, additionalParameters);
        this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
    }
    
    
    /**
     * 这个方法 父类中直接返回了空字符串,
     * 我们可以根据自己的需要重写,可以返回密码
     */
    @Override
    public Object getCredentials() {
        return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);
    }


    public Set<String> getScopes() {
        return this.scopes;
    }
}
1.4 util类

有2个,

  1. OAuth2AuthenticationProviderUtils
public class OAuth2AuthenticationProviderUtils {
    
    private OAuth2AuthenticationProviderUtils() {
    }

    public static 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);
    }

    public static <T extends OAuth2Token> OAuth2Authorization invalidate(OAuth2Authorization authorization, T token) {

        // @formatter:off
        OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
                .token(token,
                        (metadata) ->
                                metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));

        if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) {
            authorizationBuilder.token(
                    authorization.getAccessToken().getToken(),
                    (metadata) ->
                            metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));

            OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
                    authorization.getToken(OAuth2AuthorizationCode.class);
            if (authorizationCode != null && !authorizationCode.isInvalidated()) {
                authorizationBuilder.token(
                        authorizationCode.getToken(),
                        (metadata) ->
                                metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
            }
        }
        // @formatter:on

        return authorizationBuilder.build();
    }

    public static <T extends OAuth2Token> OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token,
            OAuth2TokenContext accessTokenContext) {

        OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token.getTokenValue(),
                token.getIssuedAt(), token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
        OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient()
            .getTokenSettings()
            .getAccessTokenFormat();
        builder.token(accessToken, (metadata) -> {
            if (token instanceof ClaimAccessor claimAccessor) {
                metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claimAccessor.getClaims());
            }
            metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
            metadata.put(OAuth2TokenFormat.class.getName(), accessTokenFormat.getValue());
        });

        return accessToken;
    }
}
  1. OAuth2EndpointUtils
public class OAuth2EndpointUtils {
    
    public static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";

    private OAuth2EndpointUtils() {
    }

    public static MultiValueMap<String, String> getFormParameters(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameterMap.forEach((key, values) -> {
            String queryString = StringUtils.hasText(request.getQueryString()) ? request.getQueryString() : "";
            // If not query parameter then it's a form parameter
            if (!queryString.contains(key) && values.length > 0) {
                for (String value : values) {
                    parameters.add(key, value);
                }
            }
        });
        return parameters;
    }

    public static MultiValueMap<String, String> getQueryParameters(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameterMap.forEach((key, values) -> {
            String queryString = StringUtils.hasText(request.getQueryString()) ? request.getQueryString() : "";
            if (queryString.contains(key) && values.length > 0) {
                for (String value : values) {
                    parameters.add(key, value);
                }
            }
        });
        return parameters;
    }

    public static Map<String, Object> getParametersIfMatchesAuthorizationCodeGrantRequest(HttpServletRequest request,
            String... exclusions) {
        if (!matchesAuthorizationCodeGrantRequest(request)) {
            return Collections.emptyMap();
        }
        MultiValueMap<String, String> multiValueParameters = "GET".equals(request.getMethod())
                ? getQueryParameters(request) : getFormParameters(request);
        for (String exclusion : exclusions) {
            multiValueParameters.remove(exclusion);
        }

        Map<String, Object> parameters = new HashMap<>();
        multiValueParameters.forEach(
                (key, value) -> parameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0])));

        return parameters;
    }

    public static boolean matchesAuthorizationCodeGrantRequest(HttpServletRequest request) {
        return AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
            .equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))
                && request.getParameter(OAuth2ParameterNames.CODE) != null;
    }

    public static boolean matchesPkceTokenRequest(HttpServletRequest request) {
        return matchesAuthorizationCodeGrantRequest(request)
                && request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
    }

    public static void throwError(String errorCode, String parameterName, String errorUri) {
        OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
        throw new OAuth2AuthenticationException(error);
    }

    public static String normalizeUserCode(String userCode) {
        Assert.hasText(userCode, "userCode cannot be empty");
        StringBuilder sb = new StringBuilder(userCode.toUpperCase().replaceAll("[^A-Z\\d]+", ""));
        Assert.isTrue(sb.length() == 8, "userCode must be exactly 8 alpha/numeric characters");
        sb.insert(4, '-');
        return sb.toString();
    }

    public static boolean validateUserCode(String userCode) {
        return (userCode != null && userCode.toUpperCase().replaceAll("[^A-Z\\d]+", "").length() == 8);
    }
}

这样就是添加完了。


image.png

2. 认证服务器配置

认证服务器1是需要验证账号密码,2是需要生成Token。验证账号密码还是Springsecurity的逻辑,生成token用的是jwt。所以需要配置2方面。
首先我们要把数据库导进去,在maven导入的包里面,自己可以找到:


sql

image.png
2.1 security配置

和以前说的 spring security配置差不多,要userdetail、WebSecutiryConfig、错误返回数据类等。

2.1.1 userdetail
@Slf4j
public class UserDetailService implements UserDetailsService {
    
    @Resource
    private ResourceService resourceService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Result<AuthUser> result = resourceService.loadUserByName(username);
        if(!CodeMsg.SUCCESS.getCode().equals(result.getCode())) {
            throw new UsernameNotFoundException("账号或密码错误!");
        }
        AuthUser user = BeanUtil.toBean(result.getData(), AuthUser.class) ;
        MyUserDetail userDetail = new MyUserDetail();
        BeanUtil.copyProperties(user, userDetail, false);
        if (!userDetail.isEnabled()) {
            throw new DisabledException("账号状态异常!");
        }
        
        return userDetail;
    }
}
@Data
@EqualsAndHashCode(callSuper = false)
public class MyUserDetail extends AuthUser implements UserDetails {

    private static final long serialVersionUID = 786868339462173799L;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        
        return null;
    }

    @Override
    public String getUsername() {
        return this.getUname();
    }

    @Override
    public boolean isAccountNonExpired() {
        return UserDetails.super.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return UserDetails.super.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return UserDetails.super.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return this.getStatus() == 1;
    }
}
2.1.2 WebSecutiryConfig
@Configuration
public class WebSecutiryConfig {


    @Bean
    UserDetailsService userDetailsService() {
        UserDetailService userDetail = new UserDetailService();
        return userDetail;
    }


    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
     /**
     * Spring Security 安全过滤器链配置
     */
    @Bean
    @Order(1)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        
        // 1. 开启认证,提前排除 不需要认证的
        http.authorizeHttpRequests(authorize -> {
            authorize
                    // 路径不用加context-path
                    .requestMatchers("/oauth2/**").permitAll()
                    // 登陆图形验证码
                    .requestMatchers("/captcha/**").permitAll()
                    // 登陆
                    .requestMatchers("/login/oauthlogin").permitAll()
                    // admin
                    .requestMatchers("/actuator/**").permitAll()
                    .requestMatchers("/instances/**").permitAll()
                    .anyRequest().authenticated();
                })
            
        
        
        ;
        // 2. 登陆方式:默认是使用security提供表单的登陆页面和方式,我们这里关闭
        http.formLogin(Customizer.withDefaults());
//      http.formLogin(AbstractHttpConfigurer::disable);
        
        // 3. 登出配置
//      http.logout(logout -> logout.logoutSuccessHandler(new MyLogoutSuccessHandler()));
        
        // 4. security异常错误配置
        http.exceptionHandling(exception -> {
            // 未认证时 访问接口,返回错误
            exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
            // 未授权时,返回错误,一般资源服务器 才会用到这个
            exception.accessDeniedHandler(new MyAccessDeniedHandler());
        });
        
        
        
        
        
        // 5. csrf是默认开启的,此时对于post请求,会需要一个"_csrf"的隐藏字段传递,为了前端方便,这个关了
        http.csrf(csrf -> csrf.disable());
        
        // 6. 跨域处理
        http.cors(Customizer.withDefaults());
        
        // 7. session管理,设置同一个账号只能登陆一次.单体服务这个管用,微服务 不能用
//      http.sessionManagement(session -> session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy()));

        return http.build();
    }

    /**
     * Spring Security 排除路径
     */
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return (web) ->
                // 不走过滤器链(swagger和静态资源js、css、html)
                web.ignoring().requestMatchers(
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/swagger-ui/**"
                );
    }
}
2.1.3 返回数据处理
  • MyAuthenticationEntryPoint
/**
 * 未认证(没有登录)时,返回异常 信息
 */
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        if (authException instanceof InvalidBearerTokenException) {
            System.out.println(authException.getMessage());
        }
        authException.printStackTrace();
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        
        Result<String> res = new Result<String>().error(CodeMsg.AUTHENTICATION_FAILED);
        
        httpResponseConverter.write(res, null, httpResponse);

    }
}
  • MyAccessDeniedHandler
/**
 * 登陆了,没有权限时,触发异常 返回信息
 */
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    
    private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        System.out.println("=====无权限的异常处理");
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        
        Result<Integer> res = new Result<Integer>().error(accessDeniedException.getLocalizedMessage());
        
        
        httpResponseConverter.write(res, null, httpResponse);

    }
}

security的配置好了,目录这样的:


security目录
2.2 oauth2配置

oauth2.1 主要配置了3个方面,都在AuthorizationServerConfig中:

    1. 认证服务器使用自己写的密码模式
    1. token生成:公钥、私钥、自定义字段等
    1. token和数据库交互
2.2.1 AuthorizationServerConfig
@Configuration
public class AuthorizationServerConfig {
    
    Logger log = LoggerFactory.getLogger(this.getClass()); 
    
    private static final String KEY_ID = "jnGZxjHC54hP4ZnXrrEedtNweQ6aK29w";
    
    @Resource
    private MyOAuth2TokenJwtCustomizer jwtCustomizer;
    @Resource
    private RSAKeyPair rsaKeyPair;

    /**
     * 授权服务器端点配置
     */
    @Bean
    @Order(1)
    SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
                                                            AuthenticationManager authenticationManager,
                                                            OAuth2AuthorizationService authorizationService,
                                                            OAuth2TokenGenerator<?> tokenGenerator) throws Exception {
        // 这是 http 的默认配置,可以点进去看一下,我们为了加入自己的密码模式,需要把
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
//      OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
//        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
//        http.securityMatcher(endpointsMatcher)
//                .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
//                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
//                .apply(authorizationServerConfigurer);
        
        // 添加password模式
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenEndpoint(tokenEndpoint ->
                    tokenEndpoint
                        // 添加授权模式转换器(Converter)
                        .accessTokenRequestConverter(new OAuth2PasswordAuthenticationConverter())
                        // 添加 授权模式提供者(Provider)
                        .authenticationProvider(new OAuth2PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator))
                        // 成功响应
                        .accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
                        // 失败响应
                        .errorResponseHandler(new MyAuthenticationFailureHandler()));
        
        // 开启OpenID Connect 1.0协议相关端点
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
        
        // 当使用授权码模式时,因授权码模式需要登陆,这个配置需要打开
//      http.exceptionHandling(exception -> exception
//              .defaultAuthenticationEntryPointFor(
//                  new LoginUrlAuthenticationEntryPoint("/login"),
//                  new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
//              )
//          );
        return http.build();
    }


    /**
     * 配置认证服务器请求地址
     */
    @Bean
    @Order(2)
    AuthorizationServerSettings authorizationServerSettings() {
        // 什么都不配置,则使用默认地址   
        return AuthorizationServerSettings.builder().build();
    }
    
    
    // 客户端信息表:oauth2_registered_client
    @Bean
    @Order(3)
    RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        return registeredClientRepository;
    }


    // 和 token表交互数据用的:oauth2_authorization
    @Bean
    OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {

        JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
        JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
        rowMapper.setLobHandler(new DefaultLobHandler());
        ObjectMapper objectMapper = new ObjectMapper();
        ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
        List<Module> modules = SecurityJackson2Modules.getModules(classLoader);
        objectMapper.registerModules(modules);
        objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
        // 使用刷新模式,需要从 oauth2_authorization 表反序列化attributes字段得到用户信息(SysUserDetails)
//        objectMapper.addMixIn(SysUserDetails.class, SysUserMixin.class);
        objectMapper.addMixIn(Long.class, Object.class);
        
        rowMapper.setObjectMapper(objectMapper);
        service.setAuthorizationRowMapper(rowMapper);
        return service;
    }

    // 和授权记录表交互数据用的:oauth2_authorization_consent
    @Bean
    OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }


    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    /**
     * =========   Token部分  ========
     */
    
    /**
     * 配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
     * JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
     */
    @Bean
    JWKSource<SecurityContext> jwkSource() {
        RSAPublicKey publicKey = null;
        RSAPrivateKey privateKey = null;
        String publicKeyBase64 = rsaKeyPair.getPublicKeyBase64();
        String privateKeyBase64 = rsaKeyPair.getPrivateKeyBase64();
        if (StringUtils.hasText(publicKeyBase64) && StringUtils.hasText(privateKeyBase64)) {
            publicKey = getPublicKey(publicKeyBase64);
            privateKey = getPrivateKey(privateKeyBase64);
        } else {
            KeyPair keyPair = generateRsaKey();
            publicKey = (RSAPublicKey) keyPair.getPublic();
            privateKey = (RSAPrivateKey) keyPair.getPrivate();
            log.warn("未设置生成token的秘钥!!!");
            log.info("生成临时秘钥:");
            log.info("\r\n===publicKey===" + 
                    "\r\n" + 
                    Base64.getEncoder().encodeToString(publicKey.getEncoded()) + 
                    "\r\n===privateKey===" + 
                    "\r\n" + 
                    Base64.getEncoder().encodeToString(privateKey.getEncoded()));
        }
        
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(KEY_ID)
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }
    
    private RSAPublicKey getPublicKey(String publicKeyBase64) {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBase64));
        RSAPublicKey rsaPublicKey = null;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return rsaPublicKey;
    }
    
    private RSAPrivateKey getPrivateKey(String privateKeyBase64) {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBase64));
        RSAPrivateKey rsaPrivateKey = null;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            rsaPrivateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return rsaPrivateKey;
    }
    
    /**
     *  生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
     */
    private static KeyPair generateRsaKey() { 
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * 配置jwt解析器
     */
    @Bean
    JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
    
    /**
     * 配置token生成器
     */
    @Bean
    OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
        JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
        // token 中加入自定义的字段内容
        jwtGenerator.setJwtCustomizer(jwtCustomizer);

        OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
        OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
        return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }
}
2.2.2 自定义token用到的类
  • MyOAuth2TokenJwtCustomizer
/**
 * 自定义Token包含的字段信息,
 * 通过context拿到authentication和其他信息,然后再拿到Principal(userDetails数据)或者其他
 */
@Configuration
public class MyOAuth2TokenJwtCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    @Override
    public void customize(JwtEncodingContext context) {
        JwtClaimsSet.Builder claims = context.getClaims();
        System.out.println("自定义token字段");
        Authentication authentication = context.getPrincipal();
        MyUserDetail detail = (MyUserDetail) authentication.getPrincipal();
        claims.claim("uid", detail.getUid());
        claims.claim("rcodes", detail.getRcodes());
    }
}
  • RSAKeyPair
@Component
public class RSAKeyPair {
    
    @Value("${security.token.public_key_base64:null}")
    private String publicKeyBase64;
    
    @Value("${security.token.private_key_base64:null}")
    private String privateKeyBase64;

    public String getPublicKeyBase64() {
        return publicKeyBase64;
    }

    public String getPrivateKeyBase64() {
        return privateKeyBase64;
    }
}
2.2.3 自定义返回数据类
  • MyAuthenticationFailureHandler
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
    private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        Result<String> res = new Result<String>();
        if (exception instanceof UsernameNotFoundException) {
            res.error(CodeMsg.USERNAME_OR_PASSWORD_ERROR);
        } else if (exception instanceof OAuth2AuthenticationException) {
            OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
            res.error(error.getErrorCode());
        }
        
        System.out.println("error走这个了: MyAuthenticationFailureHandler");
        
        httpResponseConverter.write(res, null, httpResponse);
    }

}
  • MyAuthenticationSuccessHandler
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    /**
     * MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换
     */
    private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
    private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
        
        OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
        OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
        Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();

        OAuth2AccessTokenResponse.Builder builder =OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()).tokenType(accessToken.getTokenType());
        if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
            builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
        }
        if (refreshToken != null) {
            builder.refreshToken(refreshToken.getTokenValue());
        }
        if (!CollectionUtils.isEmpty(additionalParameters)) {
            builder.additionalParameters(additionalParameters);
        }
        OAuth2AccessTokenResponse accessTokenResponse = builder.build();

        Map<String, Object> tokenResponseParameters = accessTokenResponseParametersConverter.convert(accessTokenResponse);
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        
        
        this.accessTokenHttpResponseConverter.write(new Result<String>().success(tokenResponseParameters), null, httpResponse);

    }
}

密码扩展基本完成了,整个结构是这样的:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容