前言
博客分享JWT,令牌配置,JWT實現SSO單點登錄
一、SpringSecurityOAuth
- 實現如下效果:
- 實現OAuth服務提供商功能:
實現一個標準的OAuth2協議中Provider角色的主要功能
- 創建成功處理器與失敗處理器
package com.zcw.security.app.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zcw.security.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName : ZcwAuthenctiationFailureHandler
* @Description : APP環境下認證失敗處理器
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:30
*/
@Component("zcwAuthenctiationFailureHandler")
@Slf4j
public class ZcwAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
/* (non-Javadoc)
* @see org.springframework.security.web.authentication.AuthenticationFailureHandler#onAuthenticationFailure(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.AuthenticationException)
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
logger.info("登錄失敗");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(exception.getMessage())));
}
}
package com.zcw.security.app.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName : ZcwAuthenticationSuccessHandler
* @Description : APP環境下認證成功處理器
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:39
*/
@Component("zcwAuthenticationSuccessHandler")
@Slf4j
public class ZcwAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.authentication.
* AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
* HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)
*/
@SuppressWarnings("unchecked")
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登錄成功");
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
throw new UnapprovedClientAuthenticationException("請求頭中無client信息");
}
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clientId對應的配置信息不存在:" + clientId);
} else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
}
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(token));
}
private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}
}
- 創建簡單響應的封裝類
package com.zcw.security.core.support;
/**
* @ClassName : SimpleResponse
* @Description : 簡單響應的封裝類
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:33
*/
public class SimpleResponse {
public SimpleResponse(Object content){
this.content = content;
}
private Object content;
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
}
- 創建配置類,注入OAuth相關信息
package com.zcw.security.app.authentication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
/**
* @ClassName : ZcwAuthorizationServerConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:37
*/
@Configuration
@EnableAuthorizationServer //添加此註解,就實現了認證服務器
public class ZcwAuthorizationServerConfig {
/**
* 如果實現了認證服務器,就可以實現4種授權模式,
* 什麼也不配置,就相當於開啓了
*/
//授權碼模式
}
-測試
我們在請求時,默認需要添加一個角色就是“ROLE_USER”
package com.zcw.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* @ClassName : DemoUserDetailsService
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:50
*/
@Component
@Transactional
@Slf4j
public class DemoUserDetailsService implements UserDetailsService, SocialUserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
/*
* (non-Javadoc)
*
* @see org.springframework.security.core.userdetails.UserDetailsService#
* loadUserByUsername(java.lang.String)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// logger.info("表單登錄用戶名:" + username);
// Admin admin = adminRepository.findByUsername(username);
// admin.getUrls();
// return admin;
return buildUser(username);
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
log.info("設計登錄用戶Id:" + userId);
return buildUser(userId);
}
private SocialUserDetails buildUser(String userId) {
// 根據用戶名查找用戶信息
//根據查找到的用戶信息判斷用戶是否被凍結
String password = passwordEncoder.encode("123456");
log.info("數據庫密碼是:"+password);
return new SocialUser(userId, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("xxx"));
}
}
- 配置我們服務提供商,需要在配置文件裏面編寫
oauth2:
client:
id: zcw
client-secret: zcwsecret
- 啓動訪問官方提供的測試接口
- 搭建資源服務器,一個註解搞定
package com.zcw.security.app.authentication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
/**
* @ClassName : ZcwResourceServerConfig
* @Description : 資源服務器
* @Author : Zhaocunwei
* @Date: 2020-06-30 17:15
*/
@Configuration
@EnableResourceServer
public class ZcwResourceServerConfig {
}
- Oauth核心流程
二、重構代碼支持Token
package com.zcw.security.app.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName : ZcwAuthenticationSuccessHandler
* @Description : APP環境下認證成功處理器
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:39
*/
@Component("zcwAuthenticationSuccessHandler")
@Slf4j
public class ZcwAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.authentication.
* AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
* HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)
*/
@SuppressWarnings("unchecked")
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登錄成功");
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
throw new UnapprovedClientAuthenticationException("請求頭中無client信息");
}
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clientId對應的配置信息不存在:" + clientId);
} else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
}
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(token));
}
private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}
}
- 簡化模式
- 授權碼模式
package com.zcw.security.core.validate.code;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
/**
* @ClassName : ValidateCodeSecurityConfig
* @Description : 校驗碼相關安全配置
* @Author : Zhaocunwei
* @Date: 2020-07-01 13:31
*/
@Component("validateCodeSecurityConfig")
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private Filter validateCodeFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class);
}
}
package com.zcw.security.core.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.zcw.security.core.properties.SecurityConstants;
/**
* @ClassName : FormAuthenticationConfig
* @Description : 表單登錄配置
* @Author : Zhaocunwei
* @Date: 2020-07-01 13:33
*/
@Component
public class FormAuthenticationConfig {
@Autowired
protected AuthenticationSuccessHandler zcwAuthenticationSuccessHandler;
@Autowired
protected AuthenticationFailureHandler zcwAuthenticationFailureHandler;
public void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)
.successHandler(zcwAuthenticationSuccessHandler)
.failureHandler(zcwAuthenticationFailureHandler);
}
}
package com.zcw.security.app.authentication;
import com.zcw.security.app.authentication.openid.OpenIdAuthenticationSecurityConfig;
import com.zcw.security.core.authentication.AuthorizeConfigManager;
import com.zcw.security.core.authentication.FormAuthenticationConfig;
import com.zcw.security.core.validate.code.ValidateCodeSecurityConfig;
import com.zcw.security.core.validate.code.sms.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* @ClassName : ZcwResourceServerConfig
* @Description : 資源服務器
* @Author : Zhaocunwei
* @Date: 2020-06-30 17:15
*/
@Configuration
@EnableResourceServer
public class ZcwResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
protected AuthenticationSuccessHandler zcwAuthenticationSuccessHandler;
@Autowired
protected AuthenticationFailureHandler zcwAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Autowired
private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;
@Autowired
private SpringSocialConfigurer zcwSocialSecurityConfig;
@Autowired
private AuthorizeConfigManager authorizeConfigManager;
@Autowired
private FormAuthenticationConfig formAuthenticationConfig;
@Override
public void configure(HttpSecurity http) throws Exception {
formAuthenticationConfig.configure(http);
http.apply(validateCodeSecurityConfig)//驗證碼的安全配置
.and()
.apply(smsCodeAuthenticationSecurityConfig)//短信驗證碼的安全配置
.and()
.apply(zcwSocialSecurityConfig)//社交登錄的安全配置
.and()
.apply(openIdAuthenticationSecurityConfig)
.and()
.csrf().disable();
authorizeConfigManager.config(http.authorizeRequests());
}
}
- 自定義認證處理的過濾器
package com.zcw.security.app.authentication.openid;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import com.zcw.security.core.properties.SecurityConstants;
/**
* @ClassName : OpenIdAuthenticationFilter
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-01 12:45
*/
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
private String openIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_OPENID;
private String providerIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_PROVIDERID;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public OpenIdAuthenticationFilter() {
super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_OPENID, "POST"));
}
// ~ Methods
// ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String openid = obtainOpenId(request);
String providerId = obtainProviderId(request);
if (openid == null) {
openid = "";
}
if (providerId == null) {
providerId = "";
}
openid = openid.trim();
providerId = providerId.trim();
OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openid, providerId);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 獲取openId
*/
protected String obtainOpenId(HttpServletRequest request) {
return request.getParameter(openIdParameter);
}
/**
* 獲取提供商id
*/
protected String obtainProviderId(HttpServletRequest request) {
return request.getParameter(providerIdParameter);
}
/**
* Provided so that subclasses may configure what is put into the
* authentication request's details property.
*
* @param request
* that an authentication request is being created for
* @param authRequest
* the authentication request object that should have its details
* set
*/
protected void setDetails(HttpServletRequest request, OpenIdAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from
* the login request.
*
* @param usernameParameter
* the parameter name. Defaults to "username".
*/
public void setOpenIdParameter(String openIdParameter) {
Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
this.openIdParameter = openIdParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter.
* If set to true, and an authentication request is received which is not a
* POST request, an exception will be raised immediately and authentication
* will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method
* will be called as if handling a failed authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getOpenIdParameter() {
return openIdParameter;
}
public String getProviderIdParameter() {
return providerIdParameter;
}
public void setProviderIdParameter(String providerIdParameter) {
this.providerIdParameter = providerIdParameter;
}
}
- 封裝登錄信息
package com.zcw.security.app.authentication.openid;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* @ClassName : OpenIdAuthenticationToken
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-01 12:46
*/
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
private String providerId;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public OpenIdAuthenticationToken(String openId, String providerId) {
super(null);
this.principal = openId;
this.providerId = providerId;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public OpenIdAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public String getProviderId() {
return providerId;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
- 驗證我們Token信息
package com.zcw.security.app.authentication.openid;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import java.util.HashSet;
import java.util.Set;
/**
* @ClassName : OpenIdAuthenticationProvider
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-01 13:36
*/
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
private SocialUserDetailsService userDetailsService;
private UsersConnectionRepository usersConnectionRepository;
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.AuthenticationProvider#
* authenticate(org.springframework.security.core.Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
Set<String> providerUserIds = new HashSet<>();
providerUserIds.add((String) authenticationToken.getPrincipal());
Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);
if(CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
String userId = userIds.iterator().next();
UserDetails user = userDetailsService.loadUserByUserId(userId);
if (user == null) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.AuthenticationProvider#
* supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> authentication) {
return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
}
public SocialUserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(SocialUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UsersConnectionRepository getUsersConnectionRepository() {
return usersConnectionRepository;
}
public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
this.usersConnectionRepository = usersConnectionRepository;
}
}
- 配置類,配置上面自定義的類
package com.zcw.security.app.authentication.openid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
/**
* @author zcw
*
*/
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private SocialUserDetailsService userDetailsService;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Override
public void configure(HttpSecurity http) throws Exception {
OpenIdAuthenticationFilter OpenIdAuthenticationFilter = new OpenIdAuthenticationFilter();
OpenIdAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
OpenIdAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
OpenIdAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
OpenIdAuthenticationProvider OpenIdAuthenticationProvider = new OpenIdAuthenticationProvider();
OpenIdAuthenticationProvider.setUserDetailsService(userDetailsService);
OpenIdAuthenticationProvider.setUsersConnectionRepository(usersConnectionRepository);
http.authenticationProvider(OpenIdAuthenticationProvider)
.addFilterAfter(OpenIdAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
- 創建後處理器,用來區分不同端用戶登錄
SocialAuthenticationFilter後處理器,用於在不同環境下個性化社交登錄的配置
package com.zcw.security.core.social;
import org.springframework.social.security.SocialAuthenticationFilter;
public interface SocialAuthenticationFilterPostProcessor {
/**
* @param socialAuthenticationFilter
*/
void process(SocialAuthenticationFilter socialAuthenticationFilter);
}
- 把上面的接口,注入到配置類裏面,進行判斷後處理器不等於空
package com.zcw.security.core.social;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* @ClassName : ZcwSpringSocialConfigurer
* @Description :繼承默認的社交登錄配置,加入自定義的後處理邏輯
* @Author : Zhaocunwei
* @Date: 2020-06-28 18:17
*/
public class ZcwSpringSocialConfigurer extends SpringSocialConfigurer {
/**
* object 就是我們放到過濾器鏈上的filter
* @param object
* @param <T>
* @return
*/
private String filterProcessesUrl;
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
public ZcwSpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
/* (non-Javadoc)
* @see org.springframework.security.config.annotation.SecurityConfigurerAdapter#postProcess(java.lang.Object)
*/
@SuppressWarnings("unchecked")
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
if (socialAuthenticationFilterPostProcessor != null) {
socialAuthenticationFilterPostProcessor.process(filter);
}
return (T) filter;
}
public String getFilterProcessesUrl() {
return filterProcessesUrl;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
return socialAuthenticationFilterPostProcessor;
}
public void setSocialAuthenticationFilterPostProcessor(
SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
}
}
@Bean
public SpringSocialConfigurer zcwSocialSecurityConfig(){
String filterPrpcessesUrl = mySecurityProperties.getSocialProperties().getFilterPrpcessesUrl();
ZcwSpringSocialConfigurer configurer = new ZcwSpringSocialConfigurer(filterPrpcessesUrl);
configurer.signupUrl(mySecurityProperties.getBrowserProperties()
.getSingUpUrl());
configurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return configurer;
}
- 後處理器接口實現,生成令牌返回給終端
package com.zcw.security.app.social.impl;
import com.zcw.security.core.social.SocialAuthenticationFilterPostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* @ClassName : AppSocialAuthenticationFilterPostProcessor
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-01 14:55
*/
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {
@Autowired
private AuthenticationSuccessHandler zcwAuthenticationSuccessHandler;
@Override
public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
socialAuthenticationFilter.setAuthenticationSuccessHandler(zcwAuthenticationSuccessHandler);
}
}
- 實現終端無session,操作
package com.zcw.security.app.social;
import com.zcw.security.app.AppSecretException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.concurrent.TimeUnit;
/**
* @ClassName : AppSingUpUtils
* @Description : app環境下替換providerSignInUtils,
* 避免由於沒有session導致讀不到社交用戶信息的問題
* @Author : Zhaocunwei
* @Date: 2020-07-01 15:05
*/
@Component
public class AppSingUpUtils {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
/**
* 緩存社交網站用戶信息到redis
* @param request
* @param connectionData
*/
public void saveConnectionData(WebRequest request, ConnectionData connectionData) {
redisTemplate.opsForValue().set(getKey(request), connectionData, 10, TimeUnit.MINUTES);
}
/**
* 將緩存的社交網站用戶信息與系統註冊用戶信息綁定
* @param request
* @param userId
*/
public void doPostSignUp(WebRequest request, String userId) {
String key = getKey(request);
if(!redisTemplate.hasKey(key)){
throw new AppSecretException("無法找到緩存的用戶社交賬號信息");
}
ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
Connection<?> connection = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId())
.createConnection(connectionData);
usersConnectionRepository.createConnectionRepository(userId).addConnection(connection);
redisTemplate.delete(key);
}
/**
* 獲取redis key
* @param request
* @return
*/
private String getKey(WebRequest request) {
String deviceId = request.getHeader("deviceId");
if (StringUtils.isBlank(deviceId)) {
throw new AppSecretException("設備id參數不能爲空");
}
return "zcw:security:social.connect." + deviceId;
}
}
package com.zcw.security.app;
/**
* @ClassName : AppSecretException
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-01 15:10
*/
public class AppSecretException extends RuntimeException {
public AppSecretException(String msg){
super(msg);
}
}
- 修改配置類
package com.zcw.security.app.social;
import com.zcw.security.core.properties.SecurityConstants;
import com.zcw.security.core.social.ZcwSpringSocialConfigurer;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* @ClassName : SpringSocialConfigurerPostProcessor
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-03 14:20
*/
@Component
public class SpringSocialConfigurerPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(StringUtils.equals(beanName, "zcwSocialSecurityConfig")){
ZcwSpringSocialConfigurer config = (ZcwSpringSocialConfigurer)bean;
config.signupUrl(SecurityConstants.DEFAULT_SOCIAL_USER_INFO_URL);
return config;
}
return bean;
}
}
- 創建我們的controller層
package com.zcw.security.app;
import com.zcw.security.app.social.AppSingUpUtils;
import com.zcw.security.browser.support.SocialUserInfo;
import com.zcw.security.core.properties.SecurityConstants;
import com.zcw.security.core.social.SocialController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName : AppSecurityController
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-03 14:24
*/
@RestController
public class AppSecurityController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@Autowired
private AppSingUpUtils appSingUpUtils;
/**
* 需要註冊時跳到這裏,返回401和用戶信息給前端
* @param request
* @return
*/
@GetMapping(SecurityConstants.DEFAULT_SOCIAL_USER_INFO_URL)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
//把用戶信息存儲到redis中
appSingUpUtils.saveConnectionData(new ServletWebRequest(request), connection.createData());
return SocialController.buildSocialUserInfo(connection);
}
}
- 創建抽象類:
package com.zcw.security.core.social;
import com.zcw.security.browser.support.SocialUserInfo;
import org.springframework.social.connect.Connection;
/**
* @ClassName : SocialController
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-03 14:25
*/
public abstract class SocialController {
/**
* 根據Connection信息構建SocialUserInfo
* @param connection
* @return
*/
public static SocialUserInfo buildSocialUserInfo(Connection<?> connection) {
SocialUserInfo userInfo = new SocialUserInfo();
userInfo.setProviderId(connection.getKey().getProviderId());
userInfo.setProviderUserId(connection.getKey().getProviderUserId());
userInfo.setNickname(connection.getDisplayName());
userInfo.setHeadimg(connection.getImageUrl());
return userInfo;
}
}
三、令牌配置
針對上面的圖片,進行自定義我們自己的Token相關操作:
- 基本的Token參數配置
- 使用JWT替換默認的token
- 擴展和解析JWT的信息
package com.zcw.security.app.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
/**
* @ClassName : ZcwAuthorizationServerConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-06-30 16:37
*/
@Configuration
@EnableAuthorizationServer //添加此註解,就實現了認證服務器
public class ZcwAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
//進行修改,配置Token的入口點
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
/**
* 配置跟客戶端相關的一些配置信息---是指有哪些應用會訪問我們服務
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("zcw")
.secret("zcwsecret")
//我們發出去的令牌有效時間是多少 單位爲:秒
.accessTokenValiditySeconds(7200)
//支持哪些授權模式
.authorizedGrantTypes("refresh_token","password")
//發出的權限有哪些
.scopes("all","read","write");
.and()
//配置另外一個客戶端提供相應的配置
.withClient("XXXXXXXXX");
}
}
- 把上面的類進行優化,抽象一下,查看可以給哪些客戶端發送令牌
package com.zcw.security.core.properties;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
/**
* @ClassName : OAuth2Properties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-03 15:10
*/
public class OAuth2Properties {
/**
* 使用jwt時爲token簽名的祕鑰
*/
private String jwtSigningKey = "zcw";
/**
* 客戶端配置
*/
private OAuth2ClientProperties[] clients = {};
public OAuth2ClientProperties[] getClients() {
return clients;
}
public void setClients(OAuth2ClientProperties[] clients) {
this.clients = clients;
}
public String getJwtSigningKey() {
return jwtSigningKey;
}
public void setJwtSigningKey(String jwtSigningKey) {
this.jwtSigningKey = jwtSigningKey;
}
}
- 認證服務器註冊的第三方應用配置項
package com.zcw.security.core.properties;
/**
* @ClassName : OAuth2ClientProperties
* @Description : 認證服務器註冊的第三方應用配置項
* @Author : Zhaocunwei
* @Date: 2020-07-03 15:11
*/
public class OAuth2ClientProperties {
/**
* 第三方應用appId
*/
private String clientId;
/**
* 第三方應用appSecret
*/
private String clientSecret;
/**
* 針對此應用發出的token的有效時間
*/
private int accessTokenValidateSeconds = 7200;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public int getAccessTokenValidateSeconds() {
return accessTokenValidateSeconds;
}
public void setAccessTokenValidateSeconds(int accessTokenValidateSeconds) {
this.accessTokenValidateSeconds = accessTokenValidateSeconds;
}
}
- 創建一個頂級的配置文件
package com.zcw.security.core.properties;
/**
* @ClassName : SecurityProperties
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-03 15:14
*/
@ConfigurationProperties(prefix = "zcw.security")
public class SecurityProperties {
/**
* 瀏覽器環境配置
*/
private BrowserProperties browser = new BrowserProperties();
/**
* 驗證碼配置
*/
private ValidateCodeProperties code = new ValidateCodeProperties();
/**
* 社交登錄配置
*/
private SocialProperties social = new SocialProperties();
/**
* OAuth2認證服務器配置
*/
private OAuth2Properties oauth2 = new OAuth2Properties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
public ValidateCodeProperties getCode() {
return code;
}
public void setCode(ValidateCodeProperties code) {
this.code = code;
}
public SocialProperties getSocial() {
return social;
}
public void setSocial(SocialProperties social) {
this.social = social;
}
public OAuth2Properties getOauth2() {
return oauth2;
}
public void setOauth2(OAuth2Properties oauth2) {
this.oauth2 = oauth2;
}
}
- 修改配置 文件
- 然後把我們的配置文件類,注入到我們認證服務器裏:
- 創建令牌存取配置類:
package com.zcw.security.app;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
/**
* @ClassName : TokenStoreConfig
* @Description : 負責令牌的存取
* @Author : Zhaocunwei
* @Date: 2020-07-03 16:16
*/
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* redis 配置類
*/
@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}
- 認證服務器裏面添加redis的注入
四、基於JWT實現SSO單點登錄
- 修改之前token類,添加JWT靜態類:
package com.zcw.security.app;
import com.zcw.security.core.properties.OAuth2Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
/**
* @ClassName : TokenStoreConfig
* @Description : 負責令牌的存取
* @Author : Zhaocunwei
* @Date: 2020-07-03 16:16
*/
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* redis 配置類
*/
@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
@Configuration
@ConditionalOnProperty( prefix = "zcw.security.oauth2",
name = "storeType",
havingValue = "jwt",
matchIfMissing =true )
public static class JwtTokenConfig{
@Autowired
private OAuth2Properties auth2Properties;
@Bean
public TokenStore jwtTokenStroe(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
//做一些Token生成處理
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
//準備祕鑰,進行簽名
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
//發出去需要這個祕鑰,簽名驗證也需要這個簽名
accessTokenConverter.setSigningKey(auth2Properties.getJwtSigningKey());
return accessTokenConverter;
}
}
}
- 測試:
- JWT 擴展,編寫實現類:
package com.zcw.security.app.jwt;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName : ZcwJwtTokenEnhancer
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 14:26
*/
public class ZcwJwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String,Object> info= new HashMap<>();
info.put("company","zcw");
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
return accessToken;
}
}
- 放到配置類中:
- 配置認證服務器,使用我們擴展的JWT,增強器
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
- 驗證簽名
- 刷新token
refresh_token是用來刷新token的,用戶無感知的情況下:
- 基於JWT實現SSO
創建新的模塊-- 認證服務器 sso-demo
<!--項目版本管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--添加springCloud依賴管理-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--添加maven的編譯插件-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<!--對編譯插件進行配置-->
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
- 創建應用A與應用B
- sso-server添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
</dependencies>
- 編寫啓動類:
package com.zcw.sso.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import javax.swing.*;
/**
* @ClassName : SsoServerApplication
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 15:19
*/
@SpringBootApplication
public class SsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class,args);
}
}
- 創建配置類:(創建認證服務器)
package com.zcw.sso.server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* @ClassName : SsoAuthorizationServerConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 15:28
*/
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer)
throws Exception{
clientDetailsServiceConfigurer.inMemory()
.withClient("zcw1")
.secret("zcwsecrect1")
.authorizedGrantTypes("authorization_code","refresh_token")
.scopes("all")
.and()
.withClient("zcw2")
.secret("zcwsecrect2")
.authorizedGrantTypes("authorization_code","refresh_token")
.scopes("all");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer)
throws Exception{
endpointsConfigurer.tokenStore(jwtTokneStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
//認證服務器的安全配置
@Override
public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfigurer)
throws Exception{
//添加授權表達式
serverSecurityConfigurer.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokneStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//簽名key
converter.setSigningKey("zcw");
return converter;
}
}
- 創建配置文件:
server:
port: 9999
context-path: /server
security:
user:
password: 123456
- 編寫應用A(應用B)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
- 創建配置文件,使我們的授權生效
添加認證服務器的地址
security:
oauth2:
client:
id: test
client-secret: test
# 認證服務器的地址
user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
# 請求令牌的地址
access-token-uri: http://127.0.0.1:9999/server/oauth/token
#拿祕鑰
resource:
jwt:
key-uri: http://127.0.0.1:9999/server/oauth/token_key
server:
port: 8099
context-path: /clientA
package com.zcw.sso.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName : SsoClientAApplication
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 16:04
*/
@SpringBootApplication
@RestController
@EnableOAuth2Sso
public class SsoClientAApplication {
@GetMapping("/user")
public Authentication user(Authentication user){
return user;
}
public static void main(String[] args) {
SpringApplication.run(SsoClientAApplication.class,args);
}
}
- 訪問clientA 直接跳轉到clientB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SSO ClientA</title>
</head>
<body>
<h1>SSO Demo Client1</h1>
<a href="http://127.0.0.1:8060/clientB/index.html">訪問ClientB</a>
</body>
</html>
clientB 與ClientA的編碼除了端口,其他都一樣
package client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName : SsoClientAApplication
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 16:04
*/
@SpringBootApplication
@RestController
@EnableOAuth2Sso
public class SsoClientBApplication {
@GetMapping("/user")
public Authentication user(Authentication user){
return user;
}
public static void main(String[] args) {
SpringApplication.run(SsoClientBApplication.class,args);
}
}
security:
oauth2:
client:
id: test
client-secret: test
# 認證服務器的地址
user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
# 請求令牌的地址
access-token-uri: http://127.0.0.1:9999/server/oauth/token
#拿祕鑰
resource:
jwt:
key-uri: http://127.0.0.1:9999/server/oauth/token_key
server:
port: 8060
context-path: /clientB
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
- 優化登錄:標準的表單登
package com.zcw.sso.server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @ClassName : SsoSecurityConfig
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 16:39
*/
@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//密碼加密器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//覆蓋默認配置 UserDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
//密碼加密器
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
package com.zcw.sso.server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @ClassName : SsoUserDetailsService
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 16:41
*/
@Component
public class SsoUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return new User(username,passwordEncoder.encode("123456"),
AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}
- 優化授權-默認操作,用戶無感知操作
需要修改源代碼使整個表單默認,自動提交
package com.zcw.sso.server;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @ClassName : SsoApprovalEndpoint
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 16:52
*/
@RestController
@SessionAttributes("authorizationRequest")
public class SsoApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SsoSpelView(template), model);
}
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
String template = TEMPLATE;
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
template = template.replace("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
else {
template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
}
if (model.containsKey("_csrf") || request.getAttribute("_csrf") != null) {
template = template.replace("%csrf%", CSRF);
}
else {
template = template.replace("%csrf%", "");
}
return template;
}
private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("<ul>");
@SuppressWarnings("unchecked")
Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request
.getAttribute("scopes"));
for (String scope : scopes.keySet()) {
String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved)
.replace("%denied%", denied);
builder.append(value);
}
builder.append("</ul>");
return builder.toString();
}
private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
private static String TEMPLATE = "<html><body><div style='display:none'><h1>OAuth Approval</h1>"
+ "<p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p>"
+ "<form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>"
+ "%denial%</div><script document.getElementById('confirmationForm').submit()></script></body></html>";
private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%'"
+ " value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";
}
package com.zcw.sso.server;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName : SsoSpelView
* @Description :
* @Author : Zhaocunwei
* @Date: 2020-07-04 16:54
*/
public class SsoSpelView implements View {
private final String template;
private final String prefix;
private final SpelExpressionParser parser = new SpelExpressionParser();
private final StandardEvaluationContext context = new StandardEvaluationContext();
private PropertyPlaceholderHelper.PlaceholderResolver resolver;
public SsoSpelView(String template) {
this.template = template;
this.prefix = new RandomValueStringGenerator().generate() + "{";
this.context.addPropertyAccessor(new MapAccessor());
this.resolver = new PropertyPlaceholderHelper.PlaceholderResolver() {
@Override
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
};
}
public SsoSpelView(String template, String prefix) {
this.template = template;
this.prefix = prefix;
}
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
Map<String, Object> map = new HashMap<String, Object>(model);
String path = ServletUriComponentsBuilder.fromContextPath(request).build()
.getPath();
map.put("path", (Object) path==null ? "" : path);
context.setRootObject(map);
String maskedTemplate = template.replace("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, resolver);
result = result.replace(prefix, "${");
response.setContentType(getContentType());
response.getWriter().append(result);
}
}