【Java】Spring Cloud OAuth2之密码模式实战

Spring Cloud OAuth2

代码地址:https://gitee.com/kkmy/kw-microservices.git
(又是一年1024,分享一下之前搭的OAuth2服务)

OAuth2依赖版本

 <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

代码工程结构

在这里插入图片描述

核心代码配置

SecurityConfig

  • 密码模式配置 BCryptPasswordEncoder
  • 自定义用户信息认证myUserDetailsService
  • 暴露authenticationManagerBean
  • 安全参数配置configure()
MyUserDetailsService

这里使用了策略模式,根据传来的系统类型,调用对应系统服务的接口

package pers.kw.config.security;

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.stereotype.Component;
import pers.kw.common.spring.utils.SpringUtils;
import pers.kw.config.oauth.context.MyParamValue;
import pers.kw.config.oauth.context.MyParamValueThreadLocal;
import pers.kw.contants.AuthParamName;
import pers.kw.enums.AuthUserTypeEnum;
import pers.kw.service.UserDetailStrategy;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义UserDetailService
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    private static final Logger log = LoggerFactory.getLogger(MyUserDetailsService.class);

    private static final List<GrantedAuthority> authorities = new ArrayList<>(2);

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        log.info("自定义UserDetailsService处理start...");
        MyParamValue paramValue = MyParamValueThreadLocal.getCurrent();
        log.info("获取自定义参数信息:{}", JSON.toJSONString(paramValue));

        String userType = paramValue.getAuthParameter(AuthParamName.USER_TYPE);
        if (StringUtils.isBlank(userType)) {
            throw new OAuth2Exception(AuthParamName.USER_TYPE + "不能为空");
        }
        if (!AuthUserTypeEnum.userTypeSet.contains(userType)) {
            throw new OAuth2Exception(AuthParamName.USER_TYPE + "错误");
        }
        AuthUserTypeEnum userTypeEnum = AuthUserTypeEnum.getEnumObjByCode(userType);
        if (userTypeEnum == null) {
            log.info("oauth服务,用户认证策略配置错误,{}:{}", AuthParamName.USER_TYPE, userType);
            throw new OAuth2Exception("认证系统异常");
        }

        try {
            UserDetailStrategy userDetailStrategy = (UserDetailStrategy) SpringUtils.getBean(Class.forName(userTypeEnum.getUserStrategy()));
            return userDetailStrategy.getUserInfoByMobile(userName,authorities);
        } catch (ClassNotFoundException e) {
            log.error("oauth服务,用户认证策略配置获取异常", e);
            throw new OAuth2Exception("认证系统异常");
        }
    }
}

AuthorizationServerConfig

  • 授权服务安全认证配置configure(AuthorizationServerSecurityConfigurer security)
    • 自定义客户端异常处理过滤器(basic方式认证)
  • 客户端信息配置configure(ClientDetailsServiceConfigurer clients)
  • 授权服务端点配置configure(AuthorizationServerEndpointsConfigurer endpoints)
    • 自定义异常信息返回值(授权码模式、密码模式)
    • 设置token请求方式
    • token信息配置
通过添加自定义过滤器,实现对oauth标准接口增加自定义参数

MyOauthAuthenticationFilter

package pers.kw.config.oauth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import pers.kw.config.oauth.context.MyParamValue;
import pers.kw.config.oauth.context.MyParamValueThreadLocal;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 通过添加自定义过滤器,实现对oauth标准接口增加自定义参数
 */
@Component
public class MyOauthAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {

    private static final Logger log = LoggerFactory.getLogger(MyOauthAuthenticationFilter.class);

    private ApplicationContext applicationContext;

    private final RequestMatcher requestMatcher;

    private static final String URL = "/oauth/token";

    public MyOauthAuthenticationFilter() {
        this.requestMatcher = new OrRequestMatcher(
                new AntPathRequestMatcher(URL, "GET"),
                new AntPathRequestMatcher(URL, "POST")
        );
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (requestMatcher.matches(request)) {
            //将自定义参数,保存到当前本地线程中
            MyParamValue paramValue = new MyParamValue();
            paramValue.setAuthParameters(request.getParameterMap());
            MyParamValueThreadLocal.set(paramValue);
            filterChain.doFilter(request, response);
            //执行完成,清除线程本地变量
            MyParamValueThreadLocal.remove();
        } else {
            filterChain.doFilter(request, response);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

自定义异常信息返回值MyWebResponseExceptionTranslator

这里的响应码一定要设置为200,若取oauth2返回的非200响应码,在微服务调用过程中,返回值无法被正常序列化

return new ResponseEntity<>(
                ExceptionResponse.fail(status,
                        e.getMessage())
                , headers,
                HttpStatus.OK);

crm调用auth服务的feign接口
在这里插入图片描述

package pers.kw.config.oauth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import pers.kw.protocol.ExceptionResponse;

import java.io.IOException;

public class MyWebResponseExceptionTranslator implements WebResponseExceptionTranslator {

    private static final Logger log = LoggerFactory.getLogger(MyWebResponseExceptionTranslator.class);

    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();


    @Override
    public ResponseEntity<ExceptionResponse> translate(Exception e) throws Exception {
        log.error("OAuth2异常处理:", e);
        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
        Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);

        if (ase != null) {
            if (ase instanceof InvalidGrantException) {
                log.info("ase:{}", ase.getMessage());
                return handleOAuth2Exception((OAuth2Exception) ase, "密码错误");
            }
            return handleOAuth2Exception((OAuth2Exception) ase);
        }

        ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
                causeChain);
        if (ase != null) {
            return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
        }

        ase = (AccessDeniedException) throwableAnalyzer
                .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        if (ase != null) {
            return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
        }

        ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(
                HttpRequestMethodNotSupportedException.class, causeChain);
        if (ase != null) {
            return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
        }

        return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
    }

    private ResponseEntity<ExceptionResponse> handleOAuth2Exception(OAuth2Exception e, String msg) throws IOException {

        int status = e.getHttpErrorCode();

        HttpHeaders headers = new HttpHeaders();
        headers.set("Cache-Control", "no-store");
        headers.set("Pragma", "no-cache");
        if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
            headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
        }
        //HttpStatus.valueOf(status)
        return new ResponseEntity<>(
                ExceptionResponse.fail(status,
                        msg)
                , headers, HttpStatus.OK
        );

    }

    private ResponseEntity<ExceptionResponse> handleOAuth2Exception(OAuth2Exception e) throws IOException {

        int status = e.getHttpErrorCode();
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cache-Control", "no-store");
        headers.set("Pragma", "no-cache");
        if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
            headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
        }

        return new ResponseEntity<>(
                ExceptionResponse.fail(status,
                        e.getMessage())
                , headers,
                HttpStatus.OK);

    }

    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
        this.throwableAnalyzer = throwableAnalyzer;
    }

    private static class ForbiddenException extends OAuth2Exception {

        public ForbiddenException(String msg, Throwable t) {
            super(msg, t);
        }

        @Override
        public String getOAuth2ErrorCode() {
            return "access_denied";
        }

        @Override
        public int getHttpErrorCode() {
            return 403;
        }

    }

    private static class ServerErrorException extends OAuth2Exception {

        public ServerErrorException(String msg, Throwable t) {
            super(msg, t);
        }

        @Override
        public String getOAuth2ErrorCode() {
            return "server_error";
        }

        @Override
        public int getHttpErrorCode() {
            return 500;
        }

    }

    private static class UnauthorizedException extends OAuth2Exception {

        public UnauthorizedException(String msg, Throwable t) {
            super(msg, t);
        }

        @Override
        public String getOAuth2ErrorCode() {
            return "unauthorized";
        }

        @Override
        public int getHttpErrorCode() {
            return 401;
        }

    }

    private static class MethodNotAllowed extends OAuth2Exception {

        public MethodNotAllowed(String msg, Throwable t) {
            super(msg, t);
        }

        @Override
        public String getOAuth2ErrorCode() {
            return "method_not_allowed";
        }

        @Override
        public int getHttpErrorCode() {
            return 405;
        }

    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud集成OAuth2密码模式需要进行以下步骤: 1. 添加依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> ``` 2. 配置授权服务器 在application.yml文件中配置OAuth2授权服务器: ``` server: port: 9090 spring: security: oauth2: client: client-id: client client-secret: secret access-token-uri: http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize resource: user-info-uri: http://localhost:8080/user ``` 3. 配置安全配置 在SecurityConfig中配置安全配置: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated() .and() .formLogin().permitAll() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER"); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } } ``` 4. 配置资源服务器 在ResourceServerConfig中配置资源服务器: ``` @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("resource"); } } ``` 5. 测试 使用Postman等工具发送POST请求到http://localhost:9090/oauth/token,请求参数如下: ``` grant_type:password username:user password:password client_id:client client_secret:secret ``` 如果授权成功,会返回access_token,使用access_token发送GET请求到http://localhost:9090/user,即可获取用户信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值