首先登錄互踢,就是同一賬號同時只能在一處登錄,所以實現方式就是沒登錄一次就更新一次token,確保之前的token失效
這里有兩種方式
1.修改源碼,將生成機制修改
下面屏蔽的代碼就是修改的代碼,這個在網上挺多的 參考:Spring Security OAuth2 實現登錄互踢 - 雲+社區 - 騰訊雲 (tencent.com)
// 重寫生成方法 @Override @Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if (existingAccessToken != null) { // 屏蔽token是否過期 // if (!existingAccessToken.isExpired()) { // this.tokenStore.storeAccessToken(existingAccessToken, authentication); // return existingAccessToken; // } // if (existingAccessToken.getRefreshToken() != null) { // refreshToken = existingAccessToken.getRefreshToken(); // this.tokenStore.removeRefreshToken(refreshToken); // } this.tokenStore.removeAccessToken(existingAccessToken); } if (refreshToken == null) { refreshToken = this.createRefreshToken(authentication); } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken; if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { refreshToken = this.createRefreshToken(authentication); } } OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken); this.tokenStore.storeAccessToken(accessToken, authentication); refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { this.tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken; }
這樣基本就滿足了這個需求,但是在開發過程中遇到一個問題,就是源碼修改后在怎么部署到項目中
剛開始不知道,將有關的源碼全部復制到項目中
如圖
其實這樣是比較重的方式,而且也沒有必要將所有代碼都引入
只需要引入最重用的那個文件,也就是 DefaultTokenServices.java,包括文件目錄都不變
直接運行,就可以看到效果了,就不貼圖了
如果覺得這種方式太重,可以重寫接口的方式
就是我要說的
直接上接口代碼,這里直接給出 登錄互踢和登錄的接口和實現
package com.adao.security.contorller; import com.alibaba.fastjson.JSONObject; import com.adao.security.common.ApiResult; import com.adao.security.common.OperationLog; import com.adao.security.service.AuthService; import io.swagger.annotations.Api; import lombok.extern.log4j.Log4j2; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.security.Principal; import java.util.Map; /** * @author adao * @version 1.0 * @date 2021/8/12 * @description 認證控制相關接口 */ @RestController @RequestMapping("/oauth") @Api(tags = "認證管理接口") @Log4j2 public class AuthController { /** * 認證服務對象 */ @Resource private AuthService authService; /** * 重寫/oauth/token接口 * * @param principal * @param parameters * @return accessToken */ @PostMapping("/token") public ApiResult postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) { // 判斷用戶名明碼是否正確,是否被鎖定等邏輯 String username = parameters.get("username"); String password = parameters.get("password"); return authService.createAccessToken(principal, parameters); } /** * 認證注銷 * * @param params token值 * @return authLogout */ @PostMapping(value = "/logout") @OperationLog(operModul = "安全認證-登錄注銷", operType = "LogOut", operDesc = "認證token信息注銷清除") public ApiResult logOut(@RequestBody JSONObject params) { String accessToken = params.getString("accessToken"); return authService.logout(accessToken); } }
AuthServiceImpl
package com.adao.security.service.impl; import com.google.common.collect.Maps; import com.adao.security.common.ApiCode; import com.adao.security.common.ApiResult; import com.adao.security.service.AuthService; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; import org.springframework.stereotype.Service; import org.springframework.web.HttpRequestMethodNotSupportedException; import java.security.Principal; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * @author adao * @version 1.0 * @date 2021/8/12 * @description Oauth2認證邏輯實現類 */ @Service("authService") @Log4j2 public class AuthServiceImpl implements AuthService { @Autowired private RedisTokenStore redisTokenStore; @Autowired private RedisTokenStore tokenStore; @Autowired private TokenEndpoint tokenEndpoint; @Override public ApiResult createAccessToken(Principal principal, Map<String, String> parameters) { // 刷新並廢棄掉當前token再重新生成token OAuth2AccessToken accessToken = null; try { accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); String discardToken = accessToken.getValue(); // 注銷token logout(discardToken); // 重新生成token accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); log.info("create accessToken" + accessToken); } catch (HttpRequestMethodNotSupportedException e) { log.error("create accessToken error" + e.getMessage()); return ApiResult.fail("鑒權失敗"); } // token信息 Map<String, Object> resultMap = Maps.newLinkedHashMap(); resultMap.put("access_token", accessToken.getValue()); resultMap.put("refresh_token", accessToken.getRefreshToken().getValue()); resultMap.put("token_type", accessToken.getTokenType()); resultMap.put("expires_in", accessToken.getExpiresIn()); resultMap.put("scope", org.apache.commons.lang3.StringUtils.join(accessToken.getScope(), ",")); resultMap.putAll(accessToken.getAdditionalInformation()); // 權限信息 List<String> list = getAuthoritiesList(accessToken); if (list != null && !list.isEmpty()) { resultMap.put("authorities", list); return ApiResult.ok(resultMap); } else { return ApiResult.fail("權限信息獲取異常", resultMap); } } /** * 通過accessToken注銷redis中用戶注冊token信息 * * @param accessToken * @return ApiResult */ @Override public ApiResult logout(String accessToken) { try { if (StringUtils.isNotBlank(accessToken)) { OAuth2AccessToken oAuth2AccessToken = redisTokenStore.readAccessToken(accessToken); if (null != oAuth2AccessToken) { redisTokenStore.removeAccessToken(oAuth2AccessToken); OAuth2RefreshToken oAuth2RefreshToken = oAuth2AccessToken.getRefreshToken(); redisTokenStore.removeRefreshToken(oAuth2RefreshToken); redisTokenStore.removeAccessTokenUsingRefreshToken(oAuth2RefreshToken); log.info("注銷成功. accessToken : {}, RefreshToken: {}", accessToken, oAuth2RefreshToken); } else { log.info("注銷失敗. 無效accessToken : {}", accessToken); return ApiResult.fail(ApiCode.ACCESS_TOKEN_INVALID); } } return ApiResult.ok(); } catch (Exception e) { log.error("用戶注銷錯誤 :" + e.getMessage()); return ApiResult.fail("用戶注銷錯誤 : " + e.getMessage()); } } /** * 獲取用戶權限信息 * * @param accessToken * @return List */ private List<String> getAuthoritiesList(OAuth2AccessToken accessToken) { // 權限信息 Collection<? extends GrantedAuthority> authorities = tokenStore.readAuthentication(accessToken).getUserAuthentication().getAuthorities(); List<String> AuthoritiesList = new ArrayList<>(); for (GrantedAuthority authority : authorities) { AuthoritiesList.add(authority.getAuthority()); } return AuthoritiesList; } }
然后嘗試多次登錄 查看token變化還有redis中的token數據
-----------------------------
補充,上面
第一種是通過邏輯來解決互踢的需求,
第二種是通過源碼的方式
如果以上兩種都不想用還有第三種
如下,重寫接口並且同時修改源碼
AuthController2
package com.adao.security.contorller; import com.alibaba.fastjson.JSONObject; import com.adao.security.common.ApiResult; import com.adao.security.common.OperationLog; import com.adao.security.service.AuthService; import io.swagger.annotations.Api; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.common.*; import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.*; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.security.Principal; import java.util.Collections; import java.util.Date; import java.util.Map; import java.util.UUID; /** * @author adao * @version 1.0 * @date 2021/8/12 * @description 認證控制相關接口 */ @RestController @RequestMapping("/oauth") @Api(tags = "認證管理接口") @Log4j2 public class AuthController2 { /** * 認證服務對象 */ @Resource private AuthService authService; @Autowired private RedisTokenStore tokenStore; @Autowired private TokenEndpoint tokenEndpoint; @Autowired private ClientDetailsService clientDetailsService; // @Autowired // private CreateTokenServiceHandler tokenServices; @Autowired public ApplicationContext applicationContext; private OAuth2RequestFactory oAuth2RequestFactory; private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator(); private TokenGranter tokenGranter; private boolean supportRefreshToken = false; private int refreshTokenValiditySeconds = 60 * 60 * 60; private int accessTokenValiditySeconds = 60 * 60; private TokenEnhancer accessTokenEnhancer; /** * 認證注銷 * * @param params token值 * @return authLogout */ @PostMapping(value = "/logout") @OperationLog(operModul = "安全認證-登錄注銷", operType = "LogOut", operDesc = "認證token信息注銷清除") public ApiResult logOut(@RequestBody JSONObject params) { String accessToken = params.getString("accessToken"); return authService.logout(accessToken); } /** * 重寫login接口 * * @param principal * @param parameters * @return * @throws HttpRequestMethodNotSupportedException */ @PostMapping("/token") public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { String username = parameters.get("username"); String password = parameters.get("password"); String grantType = parameters.get("grant_type"); String scope = parameters.get("scope"); // OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); String clientId = this.getClientId(principal); ClientDetails client = clientDetailsService.loadClientByClientId(clientId); TokenRequest tokenRequest = new TokenRequest(parameters, clientId, Collections.singleton(scope), grantType); tokenRequest = getTokenRequest(principal, parameters, tokenRequest); if (this.isRefreshTokenRequest(parameters)) { tokenRequest.setScope(OAuth2Utils.parseParameterList((String) parameters.get("scope"))); } OAuth2Request storedOAuth2Request = tokenRequest.createOAuth2Request(client); // storedOAuth2Request.getRefreshTokenRequest().s OAuth2Authentication oa = new OAuth2Authentication(storedOAuth2Request, null); // OAuth2AccessToken token = tokenServices.createAccessToken(oa); OAuth2AccessToken token = createAccessToken(oa); // OAuth2AccessToken token = tokenServices.createAccessToken(oa); // CreateTokenServiceHandler tokenServiceHandler = applicationContext.getBean(CreateTokenServiceHandler.class); // OAuth2AccessToken token = tokenServiceHandler.createAccessToken(oa); System.out.println("client :" + client.getClientId()); System.out.println("token###" + token); return this.getResponse(token); } // private TokenRequest createTokenRequest(parameters, clientId, Collections.singleton(scope), grantType){ // TokenRequest tokenRequest = new TokenRequest(parameters, clientId, Collections.singleton(scope), grantType); // } @Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if (existingAccessToken != null) { // if (!existingAccessToken.isExpired()) { // this.tokenStore.storeAccessToken(existingAccessToken, authentication); // return existingAccessToken; // } // // if (existingAccessToken.getRefreshToken() != null) { // refreshToken = existingAccessToken.getRefreshToken(); // this.tokenStore.removeRefreshToken(refreshToken); // } this.tokenStore.removeAccessToken(existingAccessToken); } if (refreshToken == null) { refreshToken = createRefreshToken(authentication); } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { refreshToken = createRefreshToken(authentication); } } OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); this.tokenStore.storeAccessToken(accessToken, authentication); refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { this.tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken; } private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString()); int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request()); if (validitySeconds > 0) { token.setExpiration(new Date(System.currentTimeMillis() + (long) validitySeconds * 1000L)); } token.setRefreshToken(refreshToken); token.setScope(authentication.getOAuth2Request().getScope()); return (OAuth2AccessToken) (accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token); } private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) { if (!this.isSupportRefreshToken(authentication.getOAuth2Request())) { return null; } else { int validitySeconds = this.getRefreshTokenValiditySeconds(authentication.getOAuth2Request()); String value = UUID.randomUUID().toString(); return (OAuth2RefreshToken) (validitySeconds > 0 ? new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis() + (long) validitySeconds * 1000L)) : new DefaultOAuth2RefreshToken(value)); } } protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) { if (this.clientDetailsService != null) { ClientDetails client = this.clientDetailsService.loadClientByClientId(clientAuth.getClientId()); Integer validity = client.getAccessTokenValiditySeconds(); if (validity != null) { return validity; } } return this.accessTokenValiditySeconds; } protected boolean isSupportRefreshToken(OAuth2Request clientAuth) { if (this.clientDetailsService != null) { ClientDetails client = this.clientDetailsService.loadClientByClientId(clientAuth.getClientId()); return client.getAuthorizedGrantTypes().contains("refresh_token"); } else { return this.supportRefreshToken; } } protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) { if (this.clientDetailsService != null) { ClientDetails client = this.clientDetailsService.loadClientByClientId(clientAuth.getClientId()); Integer validity = client.getRefreshTokenValiditySeconds(); if (validity != null) { return validity; } } return this.refreshTokenValiditySeconds; } private TokenRequest getTokenRequest(Principal principal, @RequestParam Map<String, String> parameters, TokenRequest tokenRequest) { if (!(principal instanceof Authentication)) { throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter."); } else { String clientId = this.getClientId(principal); ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId); // TokenRequest tokenRequest = createTokenRequest(parameters, authenticatedClient); if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) { throw new InvalidClientException("Given client ID does not match authenticated client"); } else { if (authenticatedClient != null) { this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); } if (!StringUtils.hasText(tokenRequest.getGrantType())) { throw new InvalidRequestException("Missing grant type"); } else if (tokenRequest.getGrantType().equals("implicit")) { throw new InvalidGrantException("Implicit grant type not supported from token endpoint"); } else { if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) { log.debug("Clearing scope of incoming token request"); tokenRequest.setScope(Collections.emptySet()); } if (this.isRefreshTokenRequest(parameters)) { tokenRequest.setScope(OAuth2Utils.parseParameterList((String) parameters.get("scope"))); } } } return tokenRequest; } } protected String getClientId(Principal principal) { Authentication client = (Authentication) principal; if (!client.isAuthenticated()) { throw new InsufficientAuthenticationException("The client is not authenticated."); } else { String clientId = client.getName(); if (client instanceof OAuth2Authentication) { clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId(); } return clientId; } } private ResponseEntity<OAuth2AccessToken> getResponse(OAuth2AccessToken accessToken) { HttpHeaders headers = new HttpHeaders(); headers.set("Cache-Control", "no-store"); headers.set("Pragma", "no-cache"); headers.set("Content-Type", "application/json;charset=UTF-8"); return new ResponseEntity(accessToken, headers, HttpStatus.OK); } private boolean isRefreshTokenRequest(Map<String, String> parameters) { return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null; } protected ClientDetailsService getClientDetailsService() { return this.clientDetailsService; } protected OAuth2RequestFactory getOAuth2RequestFactory() { return this.oAuth2RequestFactory; } private boolean isAuthCodeRequest(Map<String, String> parameters) { return "authorization_code".equals(parameters.get("grant_type")) && parameters.get("code") != null; } protected TokenGranter getTokenGranter() { return this.tokenGranter; } }
最后還有一種方式和第三種類似,但是沒有驗證
onAuthenticationSuccess
package com.adao.security.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; 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.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.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; /** * @author adao * @version 1.0 * @date 2021/8/13 * @description 群組表(Group)表控制層 */ @Log4j2 @Component public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler { @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerTokenServices authorizationServerTokenServices; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { // 1. 從請求頭中獲取 ClientId String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Basic ")) { throw new UnapprovedClientAuthenticationException("請求頭中無client信息"); } String[] tokens = this.extractAndDecodeHeader(header, request); String clientId = tokens[0]; String clientSecret = tokens[1]; TokenRequest tokenRequest = null; // 2. 通過 ClientDetailsService 獲取 ClientDetails ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); // 3. 校驗 ClientId和 ClientSecret的正確性 if (clientDetails == null) { throw new UnapprovedClientAuthenticationException("clientId:" + clientId + "對應的信息不存在"); } else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) { throw new UnapprovedClientAuthenticationException("clientSecret不正確"); } else { // 4. 通過 TokenRequest構造器生成 TokenRequest tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "custom"); } // 5. 通過 TokenRequest的 createOAuth2Request方法獲取 OAuth2Request OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); // 6. 通過 Authentication和 OAuth2Request構造出 OAuth2Authentication OAuth2Authentication auth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); // 7. 通過 AuthorizationServerTokenServices 生成 OAuth2AccessToken OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(auth2Authentication); // 8. 返回 Token log.info("登錄成功"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(token)); } private String[] extractAndDecodeHeader(String header, HttpServletRequest request) { byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); byte[] decoded; try { decoded = Base64.getDecoder().decode(base64Token); } catch (IllegalArgumentException var7) { throw new BadCredentialsException("Failed to decode basic authentication token"); } String token = new String(decoded, StandardCharsets.UTF_8); int delim = token.indexOf(":"); if (delim == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } else { return new String[]{token.substring(0, delim), token.substring(delim + 1)}; } } }