天天看点

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

在前两篇博客中介绍了OAuth2和SpringSecurity的基本知识,本篇介绍OAuth2协议实现的SSO单点登录。

项目目录结构介绍

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

单点登录时序图

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

认证服务器

认证服务器的配置和之前的差不多,把客户端信息存到了数据中,不在是内存里

  • 项目结构
    深入OAuth2 微服务下的SSO单点登录
@EnableJdbcHttpSession
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthServerConfig  extends 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<String, 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 微服务下的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, String> params = new LinkedMultiValueMap<>();
		params.add("token", token);

		HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);

		ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);

		log.info("token info :" + response.getBody().toString());

		return response.getBody();
	}
}
           

客户端代码

  • 项目接口
深入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, 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<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);

		ResponseEntity<TokenInfo> 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, String> params = new LinkedMultiValueMap<>();
				params.add("grant_type", "refresh_token");
				params.add("refresh_token", refreshToken);
				
				HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
				
				try {
					ResponseEntity<TokenInfo> 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;
	}

}