首先登录互踢,就是同一账号同时只能在一处登录,所以实现方式就是没登录一次就更新一次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)}; } } }