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的過期緩存等等,可以自由發揮;

  其實現在很多微服務都是內網通信,通過路由暴露端口了,所以可以定制一些特殊請求,來做無權限訪問?

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM