zoukankan      html  css  js  c++  java
  • OAuth2.0协议专区-Springcloud集成springsecurity oauth2实现服务统一认证,应该时最简单的教程了~

    1.配置认证服务器

    (1) 首先配置springsecurity,其实他底层是很多filter组成,顺序是请求先到他这里进行校验,然后在到oauth

    /**
    
     * @author: gaoyang
    
     * @Description: 身份认证拦截
    
     */
    
    @Order(1)
    
    @Configuration
    
    //注解权限拦截
    
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
     
    
        @Autowired
    
        UserDetailsServiceConfig userDetailsServiceConfig;
    
     
    
        //认证服务器需配合Security使用
    
        @Bean
    
        @Override
    
        public AuthenticationManager authenticationManagerBean() throws Exception {
    
            return super.authenticationManagerBean();
    
        }
    
    //websecurity用户密码和认证服务器客户端密码都需要加密算法 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //验证用户权限 auth.userDetailsService(userDetailsServiceConfig); //也可以在内存中创建用户并为密码加密 // auth.inMemoryAuthentication() // .withUser("user").password(passwordEncoder().encode("123")).roles("USER") // .and() // .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN"); } //uri权限拦截,生产可以设置为启动动态读取数据库,具体百度 @Override protected void configure(HttpSecurity http) throws Exception { http //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态 .formLogin() .and() .csrf().disable() .authorizeRequests().antMatchers("/test").permitAll() .and() .authorizeRequests().anyRequest().authenticated(); } //设置不拦截资源服务器的认证请求 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/oauth/check_token"); } }

      (2)这里的UserDetailsServiceConfig就是去校验登陆用户,可以写测试使用内存或者数据库方式读取用户信息(我这里写死了账号为user,密码为123)

    @Component
    public class UserDetailsServiceConfig implements UserDetailsService {
    
      @Autowired
      private PasswordEncoder passwordEncoder;
    
      //生产环境使用数据库进行验证
      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!username.equals("user")) {
          throw new AcceptPendingException();
        }
        return new User(username, passwordEncoder.encode("123"),
        AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
      }
    }

    (3)配置认证服务器(详见注释)

     
    /**
     * @author: gaoyang
     * @Description:认证服务器配置
     */
    @Order(2)
    @EnableAuthorizationServer
    @Configuration
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
     
        @Autowired
        AuthenticationManager authenticationManager;
        @Autowired
        BCryptPasswordEncoder bCryptPasswordEncoder;
        @Autowired
        UserDetailsServiceConfig myUserDetailsService;
     
        //为了测试客户端与凭证存储在内存(生产应该用数据库来存储,oauth有标准数据库模板)
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("client1-code") // client_id
                    .secret(bCryptPasswordEncoder.encode("123")) // client_secret
                    .authorizedGrantTypes("authorization_code") // 该client允许的授权类型
                    .scopes("app") // 允许的授权范围
                    .redirectUris("https://www.baidu.com")
                    .resourceIds("goods", "mechant")    //资源服务器id,需要与资源服务器对应
     
                    .and()
                    .withClient("client2-credentials")
                    .secret(bCryptPasswordEncoder.encode("123"))
                    .authorizedGrantTypes("client_credentials")
                    .scopes("app")
                    .resourceIds("goods", "mechant")
     
                    .and()
                    .withClient("client3-password")
                    .secret(bCryptPasswordEncoder.encode("123"))
                    .authorizedGrantTypes("password")
                    .scopes("app")
                    .resourceIds("mechant")
     
                    .and()
                    .withClient("client4-implicit")
                    .authorizedGrantTypes("implicit")
                    .scopes("app")
                    .resourceIds("mechant");
        }
     
        //配置token仓库
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            //authenticationManager配合password模式使用
            endpoints.authenticationManager(authenticationManager)
                    //这里使用内存存储token,也可以使用redis和数据库
                    .tokenStore(new InMemoryTokenStore());
            endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
            endpoints.tokenEnhancer(new TokenEnhancer() {
                @Override
                public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
                    //在返回token的时候可以加上一些自定义数据
                    DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken;
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("nickname", "测试姓名");
                    token.setAdditionalInformation(map);
                    return token;
                }
            });
        }
     
        //配置token状态查询
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求
            security.allowFormAuthenticationForClients();
        }
     
        //以下数据库配置
        /**
         *
         *     @Bean
         *     @Primary
         *     @ConfigurationProperties(prefix = "spring.datasource")
         *     public DataSource dataSource() {
         *         // 配置数据源(注意,我使用的是 HikariCP 连接池),以上注解是指定数据源,否则会有冲突
         *         return DataSourceBuilder.create().build();
         *     }
         *
         *     @Bean
         *     public TokenStore tokenStore() {
         *         // 基于 JDBC 实现,令牌保存到数据
         *         return new JdbcTokenStore(dataSource());
         *     }
         *
         *     @Bean
         *     public ClientDetailsService jdbcClientDetails() {
         *         // 基于 JDBC 实现,需要事先在数据库配置客户端信息
         *         return new JdbcClientDetailsService(dataSource());
         *     }
         *
         *     @Override
         *     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
         *         // 设置令牌
         *         endpoints.tokenStore(tokenStore());
         *     }
         *
         *     @Override
         *     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
         *         // 读取客户端配置
         *         clients.withClientDetails(jdbcClientDetails());
         *     }
         *
         */
    }

    (4) 新增自定义返回认证服务器数据:(这里只做演示,没有合理封装)

    @RestController
    @RequestMapping("/oauth")
    public class CustomResult {
     
        @Autowired
        private TokenEndpoint tokenEndpoint;
     
        @GetMapping("/token")
        public Object getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
            return this.result(principal,parameters);
        }
     
        @PostMapping("/token")
        public Object postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
            return this.result(principal,parameters);
        }
     
        public Object result(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
            ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.getAccessToken(principal, parameters);
            OAuth2AccessToken body = accessToken.getBody();
            Map<String, Object> customMap = body.getAdditionalInformation();
            String value = body.getValue();
            OAuth2RefreshToken refreshToken = body.getRefreshToken();
            Set<String> scope = body.getScope();
            int expiresIn = body.getExpiresIn();
            customMap.put("token",value);
            customMap.put("scope",scope);
            customMap.put("expiresIn",expiresIn);
            customMap.put("refreshToken",refreshToken);
            Map map = new HashMap();
            map.put("code",0);
            map.put("msg","success");
            map.put("data",customMap);
            return map;
        }
    }
    

      

    (5)添加获取token错误返回:(注意,客户端信息错误这里是拦截不到的)

    @RestControllerAdvice
    public class RestControllerExceptionAdvice {
     
        //判断oauth异常,自定义返回数据
        @ExceptionHandler
        public Object exception(OAuth2Exception e){
            //if ("invalid_client".equals(errorCode)) {
            //            return new InvalidClientException(errorMessage);
            //        } else if ("unauthorized_client".equals(errorCode)) {
            //            return new UnauthorizedClientException(errorMessage);
            //        } else if ("invalid_grant".equals(errorCode)) {
            //            return new InvalidGrantException(errorMessage);
            //        } else if ("invalid_scope".equals(errorCode)) {
            //            return new InvalidScopeException(errorMessage);
            //        } else if ("invalid_token".equals(errorCode)) {
            //            return new InvalidTokenException(errorMessage);
            //        } else if ("invalid_request".equals(errorCode)) {
            //            return new InvalidRequestException(errorMessage);
            //        } else if ("redirect_uri_mismatch".equals(errorCode)) {
            //            return new RedirectMismatchException(errorMessage);
            //        } else if ("unsupported_grant_type".equals(errorCode)) {
            //            return new UnsupportedGrantTypeException(errorMessage);
            //        } else if ("unsupported_response_type".equals(errorCode)) {
            //            return new UnsupportedResponseTypeException(errorMessage);
            //        } else {
            //            return (OAuth2Exception)("access_denied".equals(errorCode) ? new UserDeniedAuthorizationException(errorMessage) : new OAuth2Exception(errorMessage));
            //        }
            return "获取token错误";
        }
    }

    (6)添加自定义登陆及授权页面:

    @Controller
    // 必须配置该作用域设置
    @SessionAttributes("authorizationRequest")
    public class Oauth2Controller {
     
     
        private RequestCache requestCache = new HttpSessionRequestCache();
        private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
     
        @RequestMapping("/authentication/require")
        @ResponseBody
        @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
        public Map requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
     
            SavedRequest savedRequest = requestCache.getRequest(request, response);
            if (null != savedRequest) {
                String targetUrl = savedRequest.getRedirectUrl();
                System.out.println("引发跳转的请求是:" + targetUrl);
                redirectStrategy.sendRedirect(request, response, "/ologin");
            }
            //如果访问的是接口资源
            return new HashMap() {{
                put("code", 401);
                put("msg", "访问的服务需要身份认证,请引导用户到登录页");
            }};
        }
     
        @RequestMapping("/ologin")
        public String oauthLogin(){
            return "oauthLogin";
        }
     
        //授权控制器
        @RequestMapping("/oauth/confirm_access")
        public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
            Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ?
                    model.get("scopes") : request.getAttribute("scopes"));
            List<String> scopeList = new ArrayList<>();
            if (scopes != null) {
                scopeList.addAll(scopes.keySet());
            }
            model.put("scopeList", scopeList);
            return "oauthGrant";
        }
    }
    

      

      //uri权限拦截,生产可以设置为启动动态读取数据库,具体百度
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态
                    .formLogin().loginPage("/authentication/require")
                    .loginProcessingUrl("/authentication/form")
                    .passwordParameter("password")
                    .usernameParameter("username")
                    .and()
                    .csrf().disable()
     
                    .authorizeRequests().antMatchers("/test","/authentication/require","/ologin").permitAll()
                    .and()
                    .authorizeRequests().anyRequest().authenticated();
        }

    4.配置资源服务器

    (1)配置

    //配置资源服务器
    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
     
        private ObjectMapper objectMapper = new ObjectMapper();
     
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            //设置资源服务器id,需要与认证服务器对应
            resources.resourceId("mechant");
            //当权限不足时返回
            resources.accessDeniedHandler((request, response, e) -> {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.getWriter()
                        .write(objectMapper.writeValueAsString(Result.from("0001", "权限不足", null)));
            });
            //当token不正确时返回
            resources.authenticationEntryPoint((request, response, e) -> {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.getWriter()
                        .write(objectMapper.writeValueAsString(Result.from("0002", "access_token错误", null)));
            });
        }
        
        //配置uri拦截策略 
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .httpBasic().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint((req, resp, exception) -> {
                        resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                        resp.getWriter()
                                .write(objectMapper.writeValueAsString(Result.from("0002", "没有携带token", null)));
                    })
                    .and()
                    //无需登陆
                    .authorizeRequests().antMatchers("/noauth").permitAll()
                    .and()
                    //拦截所有请求,并且检查sope
                    .authorizeRequests().anyRequest().access("isAuthenticated() && #oauth2.hasScope('app')");
        }
     
        //静态内部返回类
        @Data
        static class Result<T> {
            private String code;
            private String msg;
            private T data;
     
            public Result(String code, String msg, T data) {
                this.code = code;
                this.msg = msg;
                this.data = data;
            }
     
            public static <T> Result from(String code, String msg, T data) {
                return new Result(code, msg, data);
            }
        }
    }

    (2)测试接口

    @RestController
    public class TestController {
     
        @GetMapping("ping")
        public Object test() {
            return "pong";
        }
       
        //无需登陆
        @GetMapping("noauth")
        public Object noauth() {
            return "noauth";
        }
     
    }

    (3)application.yml配置(远程向认证服务器鉴权)

    #配置向认证服务器认证权限
    security:
      oauth2:
        client:
          client-id: client3-password
          client-secret: 123
          access-token-uri: http://localhost:8082/oauth/token
          user-authorization-uri: http://localhost:8082/oauth/authorize
        resource:
          token-info-uri: http://localhost:8082/oauth/check_token

    5.测试用例~

    (1)password模式

      表单方式:(localhost:8082/oauth/token?username=user&password=123&grant_type=password&client_secret=123&client_id=client3-password)

    注意需要开启认证服务器的:

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
      //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求
      security.allowFormAuthenticationForClients();
    }

    表单加token方式:

    (2)code模式

      浏览器访问:  localhost:8082/oauth/authorize?client_id=client1-code&response_type=code

    跳转到登陆页面:

    选择允许

     

    然后跳转到之前设置的地址,并携带code:

     

    拿着code请求token:

     

    自定义登陆及授权页面:

     

     

    ## 当前这样配置的话,如果各微服务之间互相调用,则是没有权限的;所以我们可以给他加上token,例如使用feign:

    @Component
    public class OauthConfig implements RequestInterceptor {
      @Override
      public void apply(RequestTemplate requestTemplate) {
        requestTemplate.query("access_token","71f422b5-2204-4653-8a85-9cf2c62aac81");
      }
    }


      这里我写固定了,其实实现的话可以在认证服务器指定一个客户端模式,然后去动态获取token,这个token的过期缓存等等,可以自由发挥;

      其实现在很多微服务都是内网通信,通过路由暴露端口了,所以可以定制一些特殊请求,来做无权限访问?

  • 相关阅读:
    Linux中增加组和用户
    Storm重启topology,失败
    ES读写数据的工作原理
    Hive优化
    Flink提交流程和架构
    Flink中的Time与Window
    linux 中文件夹的文件按照时间倒序或者升序排列
    ElasticSearch之配置文件详解
    redis中的设置bind绑定ip不是设置白名单
    YARN调度架构中的AppMaster
  • 原文地址:https://www.cnblogs.com/liboware/p/12528420.html
Copyright © 2011-2022 走看看