springsecurity的單點登錄實現起來很容易,但是對csrf的過濾器攔截卡殼了三天,現在對這個測試Demo內容整理,希望幫助到遇到同樣問題的同學們!
現在開始講解:
一共三個項目,認證服務器A、第三方平臺B、第三方平臺C。下面分別進行說明
一、認證服務器A
先用maven構建好一個基本項目,然後進行開發;
目錄結構如下
pom引用主要加入以下四個依賴
<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>
編寫安全配置服務器
import com.security.sso.authorization.csrfHeader.CsrfHeaderFilter;
import com.security.sso.baseUtils.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.csrf.CsrfFilter;
@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(SsoSecurityConfig.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SecurityProperties securityProperties;
@Autowired
protected AuthenticationFailureHandler cstAuthenticationFailureHandler;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
logger.info("用自己定義的usersevices來驗證用戶");
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//把登錄方式改成表單登錄的形式
logger.info("-----定義表單登錄方式+自定義成功跳轉方法+自定義登錄頁面----");
/**/
http //.csrf().disable()先禁用跨站訪問功能
.formLogin()//表單登錄
.loginPage("/authentication/require")//自定義登錄跳轉方法
.loginProcessingUrl("/authentication/form")// 提交登錄表單地址(與登錄頁中提交的地址一致,就可以提交到登錄驗證服務MyUserDetailsService 中)
//.successHandler(cstAuthenticationSuccessHandler)//成功後跳轉自定義方法
.failureHandler(cstAuthenticationFailureHandler)//失敗後跳轉自定義方法
.and()
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/authentication/require",securityProperties.getBrowser().getSignInPage()).permitAll()//這個頁面不需要身份認證,其他都需要
.anyRequest()//任何請求
.authenticated()//都需要身份認證
.and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken設定到cookie
/**/
//http.formLogin().and().authorizeRequests().anyRequest().authenticated();
}
}
值得注意的是最後一行
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken設定到cookie
因爲項目採取前後端分離的開發模式,導致session功能失效,在springsecurity4以後默認開啓csrf驗證,來對應csrf的攻擊方式,關於什麼是csrf可以通過百度瞭解,springsecurity配置了相關過濾器,攔截post請求,爲了讓post請求可以通過驗證,我自定義了一個專門爲了cookie生成csrf_token傳到前臺的過濾器,在提交post請求的時候,利用js獲取了cookie中從後臺生成的csrf_token的值,作爲參數傳遞到請求中,有了這個參數,就可以通過springsecurity的csrf過濾器的驗證。這種情況後續再做詳細說明,畢竟卡殼兩天,身心俱疲。
配置認證服務器
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
//認證服務器
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(SsoAuthorizationServerConfig.class);
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
logger.info("創建兩個客戶端,爲這兩個客戶端發送授權,或者通過配置文件配置");
clients.inMemory()
.withClient("appa")
.secret("appa_ret")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.and()
.withClient("appb")
.secret("appb_ret")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all");
}
/** JWT令牌配置有關的兩個 bean **/
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("ssodemo");//tokenKey
logger.info("jwt的祕鑰是:ssodemo");
return converter;
}
//生成令牌
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
}
//安全配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//要訪問授權服務器的tokenKey(簽名祕鑰)時,要經過身份認證
//默認祕鑰是無法訪問的,這樣設置後,只要經過身份認證後,就可以拿到祕鑰
security.tokenKeyAccess("isAuthenticated()");
}
我在認證服務器定義了兩臺第三方服務器,就是文章開始所說的B 和 C 配置了他們的名稱和密碼和權限。
關於生成JWT令牌的代碼部分,重點關注密鑰的設置,因爲第三方服務器會通過拿去密鑰來解析令牌的內容,所以密鑰是很重要的,一定要注意密鑰的安全性。
最關鍵的就是這以上兩個類,如果是自定義登錄頁面的話,還有一些其他配置,接着說
跳轉登錄Controller
@RestController
public class JumpToLoginPageController {
private Logger logger = LoggerFactory.getLogger(getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {SavedRequest savedRequest = requestCache.getRequest(request, response);
redirectStrategy.sendRedirect(request,response, securityProperties.getBrowser().getSignInPage());
return new SimpleResponse("訪問的服務需要身份認證,請引導用戶到登錄頁",securityProperties.getBrowser().getSignInPage());
}
}
/**登錄失敗後的跳轉*/
@Component("cstAuthenticationFailureHandler") //spring security 默認處理器
public class CstAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { //implements AuthenticationFailureHandler
private final static Logger logger = LoggerFactory.getLogger(CstAuthenticationFailureHandler.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
//登錄失敗有很多原因,對應不同的異常
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
logger.info("登錄失敗");
//如果自定義設定返回的是JSON格式內容,就把內容返回到前臺即可
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//服務器內部異常
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(e));
}else{
super.onAuthenticationFailure(request,response,e);
}
}
}
用戶service,用於登錄用戶查詢和權限查詢必須實現接口 UserDetailsService, SocialUserDetailsService 的方法
loadUserByUsername
import com.security.sso.userPart.entity.*;
import com.security.sso.userPart.service.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
private final static Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
/********注入自定義的用戶service********/
@Autowired
private final SysUserService sysUserService;
@Autowired
private SysPermissionService sysPermissionService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysRoleUserService sysRoleUserService;
@Autowired
private SysPermissionRoleService sysPermissionRoleService;
@Autowired
MyUserDetailsService(SysUserService sysUserService){
this.sysUserService = sysUserService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//表單登錄
Sys_User sysUser = sysUserService.selectByUsername(username);
if (sysUser == null) {
throw new UsernameNotFoundException("用戶不存在!");
//返回方式二:返回帶失敗原因的數據對象
//return new User(username, userEntity.getPassword(), true,true,true,true,
// AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}else{
logger.info("用戶存在,用戶:" + username);
//把用戶的角色賦給該用戶當作該用戶的權限
List<Sys_Role_User> sruList = sysRoleUserService.selectByUser_id(sysUser.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
for(Sys_Role_User ru : sruList){
Sys_Role role = sysRoleService.selectRoleById(ru.getSys_role_id());
logger.info(username+"-->role:"+role.getName());
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName());
//1:此處將權限信息添加到 GrantedAuthority 對象中,在後面進行全權限驗證時會使用GrantedAuthority 對象。
grantedAuthorities.add(grantedAuthority);
}
return new User(sysUser.getUsername(), sysUser.getPassword(), grantedAuthorities);
}
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
return null;
}
}
再UserDetailsService裏登錄成功後查詢出用戶的權限(角色)把這些權限放到該用戶的授權信息中,返回
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName()); grantedAuthorities.add(grantedAuthority);
用戶登錄後要自動彈出確認授權的頁面,需要用戶自己選擇是否要確認授權。爲了給用戶更好的體驗,重寫了確認授權的頁面彈出方式,讓該確認頁面改爲自動提交的方式
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.Iterator;
import java.util.Map;
/**拷貝自源碼**/
/**org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint**/
/**需要授權的時候本來源碼要彈出確認授權表單頁面,現在改造成**/
@RestController
@SessionAttributes("authorizationRequest")
public class SsoApprovalEndpoint {
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;'>" + //增加DIV 把HTML內容隱藏
"<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>";
public SsoApprovalEndpoint() {
}
@RequestMapping({"/oauth/confirm_access"})
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = this.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%", "").replace("%denial%", DENIAL);
} else {
template = template.replace("%scopes%", this.createScopes(model, request)).replace("%denial%", "");
}
if (!model.containsKey("_csrf") && request.getAttribute("_csrf") == null) {
template = template.replace("%csrf%", "");
} else {
template = template.replace("%csrf%", CSRF);
}
return template;
}
private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("<ul>");
Map<String, String> scopes = (Map)((Map)(model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")));
Iterator var5 = scopes.keySet().iterator();
while(var5.hasNext()) {
String scope = (String)var5.next();
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();
}
}
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.util.PropertyPlaceholderHelper.PlaceholderResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/**拷貝自源碼**/
/**org.springframework.security.oauth2.provider.endpoint.SpelView**/
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 PlaceholderResolver resolver;
public SsoSpelView(String template) {
this.template = template;
this.prefix = (new RandomValueStringGenerator()).generate() + "{";
this.context.addPropertyAccessor(new MapAccessor());
this.resolver = new PlaceholderResolver() {
public String resolvePlaceholder(String name) {
Expression expression = SsoSpelView.this.parser.parseExpression(name);
Object value = expression.getValue(SsoSpelView.this.context);
return value == null ? null : value.toString();
}
};
}
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, Object> map = new HashMap(model);
String path = ServletUriComponentsBuilder.fromContextPath(request).build().getPath();
map.put("path", path == null ? "" : path);
this.context.setRootObject(map);
String maskedTemplate = this.template.replace("${", this.prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(this.prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, this.resolver);
result = result.replace(this.prefix, "${");
response.setContentType(this.getContentType());
response.getWriter().append(result);
}
}
現在重點說說CSRF的攔截器的修改,找到csrf攔截器看看源碼是怎麼對請求進行處理的
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
解釋:先從session中拿到csrfToken的值,如果是第一次進入該方法,csrfToken是不存在的,拿到的是null
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
解釋:若果拿到爲null就根據本次請求的地址生成Token,並把該Token存儲到session中
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
解釋:把csrfToken放到request中
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
解釋:如果該請求時非post請求,直接進入後續流程
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
解釋:如果是POST請求,重request中獲取csrfToken傳給actualToken
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
解釋:驗證從request中傳過來的csrfToken和本次方法中的csrfToken是不是一致,不一致的話,拋出異常信息
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
因爲是前後端分離的項目,沒有session的概念,也無法從session中獲取信息,參考了官網的解決辦法,把csrfToken放到cookie中,於是增加一個往cookie中加csrfToken的過濾器,該過濾器把csrfToken的信息放到cookie,並把cookie傳到前臺;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CsrfHeaderFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "CSRF-TOKEN");
String token = csrf.getToken();
if (cookie==null || token!=null && !token.equals(cookie.getValue())) {
cookie = new Cookie("CSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}
這樣把csrfToken的內容就可以通過前臺JS獲取,通過請求發送到後臺,這樣就會通過CsrfFilter的驗證,從而POST請求可以正常執行,這就有了前邊的把該過濾器註冊到過濾器鏈中
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken設定到cookie
下面說前臺登錄頁面的寫法
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登錄</title>
<style>
body{ text-align:center}
.div{ margin:0 auto; width:400px; height:100px; border:1px solid #F00}
</style>
</head>
<body style="background-color: #f1f1f1; padding-bottom: 0">
<form action="/server/authentication/form" method="post">
<div class="div">
<table>
<tr>
<td>用戶名:</td>
<td>
<input class="cookie" type="hidden" name="_csrf">
<input class="username" type="text" name="username">
</td>
</tr>
<tr>
<td>密碼:</td>
<td><input class="password" type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登錄</button></td>
</tr>
</table>
</div>
</form>
</body>
<script type="text/javascript">
var username = document.getElementsByClassName('usesrname')[0]
var password = document.getElementsByClassName('password')[0]
var cookie = getCookie('CSRF-TOKEN')
console.log(cookie)
document.getElementsByClassName('cookie')[0].value = cookie
function getCookie(name)
{
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
if(arr=document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}
//格式化參數
function formatParams(data) {
var arr = [];
for (var name in data) {
arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
}
arr.push(("v=" + Math.random()).replace(".",""));
return arr.join("&");
}
</script>
</html>
可以看到前臺通過js從cookie 中拿到csrfToken的信息(_csrf),跟用戶名和密碼一起發送到後臺,這樣過濾器就可以拿到了csrfToken,從而通過驗證。
服務器設置要點講述完畢。
二、第三方平臺B
pom
<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>
該項目結構
訪問資源之前必須先登錄到認證服務器中進行認證,所以配置文件中必須找到認證服務器的相關服務接口位置
server.port = 9002
server.context-path = /appa
#客戶端應用
security.oauth2.client.client-id = appa
security.oauth2.client.client-secret = appa_ret
security.basic.enabled = false
#認證服務器地址
security.oauth2.client.user-authorization-uri = http://127.0.0.1:9001/server/oauth/authorize
#向認證服務器請求令牌的地址
security.oauth2.client.access-token-uri = http://127.0.0.1:9001/server/oauth/token
#認證服務器拿回祕鑰
security.oauth2.resource.jwt.key-uri = http://127.0.0.1:9001/server/oauth/token_key
定義資源controller
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@EnableOAuth2Sso //單點登錄註解
@RestController
@RequestMapping("/user")
public class UserController {
private final static Logger logger = LoggerFactory.getLogger(UserController.class);
//獲取當前人的認證信息
@GetMapping("/me")
public Object getCurrentUser(Authentication user, HttpServletRequest request){
return user;
}
/*****************測試用戶訪問菜單權限*****************/
@PostMapping("/menu_a")
public String menu_a(){
logger.info(" into '/user/menu_a' method ");
return "this is menu_a";
}
@PostMapping("/menu_b")
public String menu_b(){
logger.info(" into '/user/menu_b' method ");
return "this is menu_b";
}
@PostMapping("/menu_c")
public String menu_c(){
logger.info(" into '/user/menu_c' method ");
return "this is menu_c";
}
}
關鍵點是單點登錄註解 @EnableOAuth2Sso //單點登錄註解
下面還是說說關於csrfToken的設置,劃重點!!!
定義過濾器
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CsrfHeaderFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//把csrfToken設定到cookie中發送到前臺
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
//Cookie cookie = WebUtils.getCookie(request, "CSRF-TOKEN");
String token = csrf.getToken();
Cookie cookie = new Cookie("X-CSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(request, response);
}
}
註冊該過濾器
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CsrfFilterConfig {
@Bean
public FilterRegistrationBean filterRegist() {
FilterRegistrationBean frBean = new FilterRegistrationBean();
frBean.setFilter(new CsrfHeaderFilter());
frBean.addUrlPatterns("/*");
System.out.println("註冊CSRFCOOKIE過濾器");
return frBean;
}
}
跟認證服務器一樣訪問資源之前先把cookie中的csrfToken傳遞到前端,然後通過js把csrfToken傳遞到後臺從而通過驗證
csrfToken是跟隨該用戶這次登錄操作的,如果用戶退出登錄,或者瀏覽器清除了cookie,就需要重新獲取該Token了
編寫頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSO Client appa</title>
</head>
<body>
<h1>SSO Client appa ==》》 appb</h1>
<a href="http://127.0.0.1:9003/appb/index.html">訪問 appb </a>
</body>
</html>
內容要點就以上
三、第三方平臺C
與平臺B類似,下面只貼一下不同點
server.port = 9003
server.context-path = /appb
#客戶端應用
security.oauth2.client.client-id = appb
security.oauth2.client.client-secret = appb_ret
security.basic.enabled = false
#認證服務器地址
security.oauth2.client.user-authorization-uri = http://127.0.0.1:9001/server/oauth/authorize
#向認證服務器請求令牌的地址
security.oauth2.client.access-token-uri = http://127.0.0.1:9001/server/oauth/token
#認證服務器拿回祕鑰
security.oauth2.resource.jwt.key-uri = http://127.0.0.1:9001/server/oauth/token_key
前端頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSO Client appb</title>
</head>
<body>
<h1>SSO Client appb ==》》 appa</h1>
<a href="http://127.0.0.1:9002/appa/index.html">訪問 appa </a>
</body>
</html>
下面演示一下登錄的過程
啓動三個服務,先啓動認證服務,然後再啓動第三方服務
地址欄輸入要訪問的第三方服務資源 http://127.0.0.1:9002/appa/index.html 因爲所有資源都收到springsecurity的保護,所有地址會跳轉到認證服務器的登錄頁面
從這裏可以看到它是如何一步一步跳轉過來的
輸入用戶信息點擊登錄,登錄成功,跳轉回所要訪問的資源頁面
點擊鏈接就可以訪問第三方平臺C,而不用再登錄第三方平臺C,系統會自動進行登錄
這樣就可以以在兩個系統之間跳轉
下面來說一下訪問第三方服務的其他資源的操作,主要是csrfToken的獲取
我們知道只有當發起POST請求才必須通過csrfToken的驗證,如果發送的是GET請求,只要滿足權限要求就可以訪問,比如說訪問B平臺的 http://127.0.0.1:9002/appa/user/me 這個請求,就可以直接從地址欄中輸入地址發送GET請求獲取數據
重點看一下response Cookie 裏邊已經獲取到了csrfToken的值,如果我們要發送post請求到後臺,這個值就非常重要了,下面是發送post請求的過程,我們用restlet來發送post請求
在發送請求時帶上了從cookie中獲取的csrfToken的值,給_csrf參數賦值後一併發送到後臺,才能正確獲得請求的數據
如果不帶csrfToken就會報錯 403 表示理解你的請求但是不能處理
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'
當時查了很多資料,走了彎路,因爲大部分資料都是講怎樣在吧.csrf().disable();禁用了,然後我在認證服務器那裏禁用了,但是方位第三方平臺還報,後來發現認證服務器只是禁用了自己,不管第三方平臺,然後我又在第三方平臺也禁用了,就是自己寫了一個SsoSecurityConfig 跟認證服務器一樣.csrf().disable();,結果第三方平臺不走單點登錄了,開始走自己的認證服務了,又趕緊把這個類刪除了,最後纔想出了自己寫過濾器的辦法,還是要看官方文檔啊,雖然官方文檔寫的不是前後端分離的項目,只是說明表單提交要加隱藏域把csrfToken加進去。只能自己又做了一些改動,到了現在這個樣子。
頁面測試
源碼下載:https://gitee.com/zhangchai/ssodemo.git