OAuth2|深入OAuth2 微服务下的SSO单点登录

在前两篇博客中介绍了OAuth2和SpringSecurity的基本知识,本篇介绍OAuth2协议实现的SSO单点登录。
项目目录结构介绍 OAuth2|深入OAuth2 微服务下的SSO单点登录
文章图片

  • sso-parent 父工程
  • order-service 订单微服务
  • sso-auth-server sso认证服务器
  • sso-gateway 微服务网关
  • sso-client 客户端(第三方应用)
单点登录时序图 OAuth2|深入OAuth2 微服务下的SSO单点登录
文章图片

认证服务器 【OAuth2|深入OAuth2 微服务下的SSO单点登录】认证服务器的配置和之前的差不多,把客户端信息存到了数据中,不在是内存里
  • 项目结构
    OAuth2|深入OAuth2 微服务下的SSO单点登录
    文章图片
@EnableJdbcHttpSession @Configuration @EnableAuthorizationServer public class OAuth2AuthServerConfigextends AuthorizationServerConfigurerAdapter { @Autowired DataSource dataSource; @Qualifier UserDetailsService userDetailsService; @Autowired private AuthenticationManager authenticationManager; @Bean public TokenStore tokenStore () { return new JdbcTokenStore(dataSource); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) .userDetailsService(userDetailsService) .authenticationManager(authenticationManager); }}

@Configuration public class OAuth2LogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String redirectUri = request.getParameter("redirect_uri"); if(!StringUtils.isEmpty(redirectUri)) { response.sendRedirect(redirectUri); } } }

@Configuration public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic().and() .logout() .logoutSuccessHandler(logoutSuccessHandler); }}

@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Autowired JdbcTemplate jdbcTemplate; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Map, Object> entity = jdbcTemplate .queryForMap("SELECT * FROM SYS_USER WHERE `USERNAME` = ?", username); if (Objects.nonNull(entity)) { return new User((String) entity.get("username"), (String) entity.get("password"), AuthorityUtils.createAuthorityList("ROLE_ADMIN")); }return null; }}

@SpringBootApplication public class AuthServerApplication { public static void main(String[] args) { SpringApplication.run(AuthServerApplication.class, args); } }

网关
  • 代码结构
    OAuth2|深入OAuth2 微服务下的SSO单点登录
    文章图片
@Slf4j @Component public class AuthorizationFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 2; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { log.info("authorization start"); RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); if(isNeedAuth(request)) {TokenInfo tokenInfo = (TokenInfo)request.getAttribute("tokenInfo"); if(tokenInfo != null && tokenInfo.isActive()) { if(!hasPermission(tokenInfo, request)) { log.info("audit log update fail 403"); handleError(403, requestContext); }requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name()); } else { if(!StringUtils.startsWith(request.getRequestURI(), "/token")) { log.info("audit log update fail 401"); handleError(401, requestContext); } } }return null; } private boolean isNeedAuth(HttpServletRequest request) { return true; } private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) { return true; } private void handleError(int status, RequestContext requestContext) { requestContext.getResponse().setContentType("application/json"); requestContext.setResponseStatusCode(status); requestContext.setResponseBody("{\"message\":\"auth fail\"}"); // 这个请求最终不会被zuul转发到后端服务器 requestContext.setSendZuulResponse(false); } }

@Slf4j @Component public class OAuthFilter extends ZuulFilter { RestTemplate restTemplate = new RestTemplate(); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException {log.info("oauth start"); // 为了获取request请求 RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); // 获取token请求不进行过滤 if(StringUtils.startsWith(request.getRequestURI(), "/token")) { return null; }String authHeader = request.getHeader("Authorization"); if(StringUtils.isBlank(authHeader)) { return null; }if(!StringUtils.startsWithIgnoreCase(authHeader, "bearer ")) { return null; }// 如果请求头带了以barer开头的请求,则去认证服务器校验token try {TokenInfo info = getTokenInfo(authHeader); request.setAttribute("tokenInfo", info); } catch (Exception e) { log.error("get token info fail", e); }return null; } /** * 校验token * @param authHeader * @return */ private TokenInfo getTokenInfo(String authHeader) {String token = StringUtils.substringAfter(authHeader, "bearer "); String oauthServiceUrl = "http://localhost:9090/oauth/check_token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("gateway", "123456"); MultiValueMap, String> params = new LinkedMultiValueMap<>(); params.add("token", token); HttpEntity> entity = new HttpEntity<>(params, headers); ResponseEntity response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class); log.info("token info :" + response.getBody().toString()); return response.getBody(); } }

客户端代码
  • 项目接口
OAuth2|深入OAuth2 微服务下的SSO单点登录
文章图片

@SpringBootApplication @RestController @EnableZuulProxy @Slf4j public class ClientApplication { private RestTemplate restTemplate = new RestTemplate(); @PostMapping("/logout") public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException { request.getSession().invalidate(); } @GetMapping("/me") public TokenInfo me(HttpServletRequest request) { TokenInfo info = (TokenInfo)request.getSession().getAttribute("token"); return info; } @GetMapping("/oauth/callback") public void callback (@RequestParam String code, String state, HttpServletRequest request, HttpServletResponse response) throws IOException {log.info("state is "+state); String oauthServiceUrl = "http://gateway.pipiha.com:9070/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("admin", "123456"); MultiValueMap, String> params = new LinkedMultiValueMap<>(); params.add("code", code); params.add("grant_type", "authorization_code"); params.add("redirect_uri", "http://admin.pipiha.com:8080/oauth/callback"); HttpEntity> entity = new HttpEntity<>(params, headers); ResponseEntity token = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class); //request.getSession().setAttribute("token", token.getBody().init()); Cookie accessTokenCookie = new Cookie("pipiha_access_token", token.getBody().getAccess_token()); accessTokenCookie.setMaxAge(token.getBody().getExpires_in().intValue()); accessTokenCookie.setDomain("pipiha.com"); accessTokenCookie.setPath("/"); response.addCookie(accessTokenCookie); Cookie refreshTokenCookie = new Cookie("pipiha_refresh_token", token.getBody().getRefresh_token()); refreshTokenCookie.setMaxAge(2592000); refreshTokenCookie.setDomain("pipiha.com"); refreshTokenCookie.setPath("/"); response.addCookie(refreshTokenCookie); response.sendRedirect("/"); } public static void main(String[] args) { SpringApplication.run(ClientApplication.class, args); }}

@Component public class CookieTokenFilter extends ZuulFilter { private RestTemplate restTemplate = new RestTemplate(); @Override public boolean shouldFilter() { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); return !StringUtils.equals(request.getRequestURI(), "/logout"); } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); //HttpServletRequest request = requestContext.getRequest(); HttpServletResponse response = requestContext.getResponse(); String accessToken = getCookie("pipiha_access_token"); if(StringUtils.isNotBlank(accessToken)) { requestContext.addZuulRequestHeader("Authorization", "bearer "+accessToken); } else { String refreshToken = getCookie("pipiha_refresh_token"); if(StringUtils.isNotBlank(refreshToken)) { String oauthServiceUrl = "http://gateway.pipiha.com:9070/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("admin", "123456"); MultiValueMap, String> params = new LinkedMultiValueMap<>(); params.add("grant_type", "refresh_token"); params.add("refresh_token", refreshToken); HttpEntity> entity = new HttpEntity<>(params, headers); try { ResponseEntity newToken = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class); //request.getSession().setAttribute("token", newToken.getBody().init()); requestContext.addZuulRequestHeader("Authorization", "bearer "+newToken.getBody().getAccess_token()); Cookie accessTokenCookie = new Cookie("pipiha_access_token", newToken.getBody().getAccess_token()); accessTokenCookie.setMaxAge(newToken.getBody().getExpires_in().intValue()); accessTokenCookie.setDomain("pipiha.com"); accessTokenCookie.setPath("/"); response.addCookie(accessTokenCookie); Cookie refreshTokenCookie = new Cookie("pipiha_refresh_token", newToken.getBody().getRefresh_token()); refreshTokenCookie.setMaxAge(2592000); refreshTokenCookie.setDomain("pipiha.com"); refreshTokenCookie.setPath("/"); response.addCookie(refreshTokenCookie); } catch (Exception e) { requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(500); requestContext.setResponseBody("{\"message\":\"refresh fail\"}"); requestContext.getResponse().setContentType("application/json"); }} else { requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(500); requestContext.setResponseBody("{\"message\":\"refresh fail\"}"); requestContext.getResponse().setContentType("application/json"); } }return null; } private String getCookie(String name) { String result = null; RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if(StringUtils.equals(name, cookie.getName())) { result = cookie.getValue(); break; } }return result; } @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; }}

    推荐阅读