在前兩篇部落格中介紹了OAuth2和SpringSecurity的基本知識,本篇介紹OAuth2協定實作的SSO單點登入。
項目目錄結構介紹

- sso-parent 父工程
- order-service 訂單微服務
- sso-auth-server sso認證伺服器
- sso-gateway 微服務網關
- sso-client 用戶端(第三方應用)
單點登入時序圖
認證伺服器
認證伺服器的配置和之前的差不多,把用戶端資訊存到了資料中,不在是記憶體裡
- 項目結構
深入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();
}
}
用戶端代碼
- 項目接口
@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;
}
}