摘要:
本文主要讲一下在企业公司内部利用oauth2的记住我这个问题,本来该系列文章不打算在更新了,可是公司内部需要记住我这个功能的实现,只好花了2天时间在研读了源码,特地分享给大家。
记住我功能实现
请大家参考社区 Spring Security 从入门到进阶系列教程的Spring Security源码分析七:Spring Security 记住我
问题:
首先在开发中大家用得比较多的是ajax传输数据,最近几年的Thymeleaf技术可能不那么普遍,在记住我这个功能实现的过程中会进行授权页的跳转流程,由于在前面的文章中我用的是前台生成form表单进行自动跳转,具体代码如下:
@RequestMapping({ "/oauth/my_approval"})
@ResponseBody
public JSONObject getAccessConfirmation(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
@SuppressWarnings("unchecked")
Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes"));
List<String> scopeList = new ArrayList<>();
for (String scope : scopes.keySet()) {
scopeList.add(scope);
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("scopeList",scopeList);
return jsonObject;
}
function auth(){
//定义一个form表单
var form=$("<form>");
$(document.body).append(form);
form.attr("method","post");
form.attr("action","../oauth/authorize");
var inputUser=$("<input>");
inputUser.attr("type","hidden");
inputUser.attr("name","user_oauth_approval");
inputUser.attr("value","true");
for(var i = 0;i<scopeList.length;i++){
var input = $("<input>");
input.attr("type","hidden");
input.attr("name",""+scopeList[i]+"")
input.attr("value","true");
form.append(input);
}
form.append(inputUser);
form.submit();//表单提交
}
具体可以参考我的github相关spring4all中相关代码
细心的人会发现这个跳转是由于前段点击事件造成的,如果是记住我功能的话,不会经过前端的事件触发后自动去登录,然后到这个授权地方来,这样的话我们是无法实现/oauth/authorize
认证流程的。参考源码如下:
//..........省略相关代码
@FrameworkEndpoint
@SessionAttributes({"authorizationRequest"})
public class AuthorizationEndpoint extends AbstractEndpoint {
//..........省略相关代码
@RequestMapping({"/oauth/authorize"})
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) {
AuthorizationRequest authorizationRequest = this.getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
} else if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
} else {
try {
if (principal instanceof Authentication && ((Authentication)principal).isAuthenticated()) {
ClientDetails client = this.getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
String redirectUriParameter = (String)authorizationRequest.getRequestParameters().get("redirect_uri");
String resolvedRedirect = this.redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException("A redirectUri must be either supplied or preconfigured in the ClientDetails");
} else {
authorizationRequest.setRedirectUri(resolvedRedirect);
this.oauth2RequestValidator.validateScope(authorizationRequest, client);
authorizationRequest = this.userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication)principal);
boolean approved = this.userApprovalHandler.isApproved(authorizationRequest, (Authentication)principal);
authorizationRequest.setApproved(approved);
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return this.getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(this.getAuthorizationCodeResponse(authorizationRequest, (Authentication)principal));
}
}
model.put("authorizationRequest", authorizationRequest);
return this.getUserApprovalPageResponse(model, authorizationRequest, (Authentication)principal);
}
} else {
throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed.");
}
} catch (RuntimeException var11) {
sessionStatus.setComplete();
throw var11;
}
}
}
@RequestMapping(
value = {"/oauth/authorize"},
method = {RequestMethod.POST},
params = {"user_oauth_approval"}
)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model, SessionStatus sessionStatus, Principal principal) {
if (!(principal instanceof Authentication)) {
sessionStatus.setComplete();
throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorizing an access token.");
} else {
AuthorizationRequest authorizationRequest = (AuthorizationRequest)model.get("authorizationRequest");
if (authorizationRequest == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
} else {
RedirectView var8;
try {
Set<String> responseTypes = authorizationRequest.getResponseTypes();
authorizationRequest.setApprovalParameters(approvalParameters);
authorizationRequest = this.userApprovalHandler.updateAfterApproval(authorizationRequest, (Authentication)principal);
boolean approved = this.userApprovalHandler.isApproved(authorizationRequest, (Authentication)principal);
authorizationRequest.setApproved(approved);
if (authorizationRequest.getRedirectUri() == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
}
if (authorizationRequest.isApproved()) {
View var12;
if (responseTypes.contains("token")) {
var12 = this.getImplicitGrantResponse(authorizationRequest).getView();
return var12;
}
var12 = this.getAuthorizationCodeResponse(authorizationRequest, (Authentication)principal);
return var12;
}
var8 = new RedirectView(this.getUnsuccessfulRedirect(authorizationRequest, new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")), false, true, false);
} finally {
sessionStatus.setComplete();
}
return var8;
}
}
}
private ModelAndView getUserApprovalPageResponse(Map<String, Object> model, AuthorizationRequest authorizationRequest, Authentication principal) {
this.logger.debug("Loading user approval page: " + this.userApprovalPage);
model.putAll(this.userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal));
return new ModelAndView(this.userApprovalPage, model);
}
private ModelAndView getImplicitGrantResponse(AuthorizationRequest authorizationRequest) {
try {
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(authorizationRequest, "implicit");
OAuth2Request storedOAuth2Request = this.getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
OAuth2AccessToken accessToken = this.getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
if (accessToken == null) {
throw new UnsupportedResponseTypeException("Unsupported response type: token");
} else {
return new ModelAndView(new RedirectView(this.appendAccessToken(authorizationRequest, accessToken), false, true, false));
}
} catch (OAuth2Exception var5) {
return new ModelAndView(new RedirectView(this.getUnsuccessfulRedirect(authorizationRequest, var5, true), false, true, false));
}
}
//..........省略相关代码
private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) {
try {
return new RedirectView(this.getSuccessfulRedirect(authorizationRequest, this.generateCode(authorizationRequest, authUser)), false, true, false);
} catch (OAuth2Exception var4) {
return new RedirectView(this.getUnsuccessfulRedirect(authorizationRequest, var4, false), false, true, false);
}
}
//..........省略相关代码
private ModelAndView handleException(Exception e, ServletWebRequest webRequest) throws Exception {
ResponseEntity<OAuth2Exception> translate = this.getExceptionTranslator().translate(e);
webRequest.getResponse().setStatus(translate.getStatusCode().value());
if (!(e instanceof ClientAuthenticationException) && !(e instanceof RedirectMismatchException)) {
AuthorizationRequest authorizationRequest = null;
try {
authorizationRequest = this.getAuthorizationRequestForError(webRequest);
String requestedRedirectParam = (String)authorizationRequest.getRequestParameters().get("redirect_uri");
String requestedRedirect = this.redirectResolver.resolveRedirect(requestedRedirectParam, this.getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId()));
authorizationRequest.setRedirectUri(requestedRedirect);
String redirect = this.getUnsuccessfulRedirect(authorizationRequest, (OAuth2Exception)translate.getBody(), authorizationRequest.getResponseTypes().contains("token"));
return new ModelAndView(new RedirectView(redirect, false, true, false));
} catch (OAuth2Exception var8) {
return new ModelAndView(this.errorPage, Collections.singletonMap("error", translate.getBody()));
}
} else {
return new ModelAndView(this.errorPage, Collections.singletonMap("error", translate.getBody()));
}
}
//..........省略相关代码
}
源码内部实现了ModelAndView
进行跳转,其实可以进行设置让他显示出来,根据这个思想我们自己也可以实现一个View
进行跳转
流程处理
思想:无论是正常登陆流程还是记住我功能流程,直接跳转到一个页面将scope
和user_oauth_approval
等数据填充,然后自动提交。
首先前台得使用表单提交否则不能生效,这个地方让我卡住了很久,如果是ajax的话会在error函数中返回整个页面的代码,因为在自定义授权页返回的地方是返回的视图。
来看一下具体的代码吧
@RequestMapping({ "/oauth/my_approval"})
public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
@SuppressWarnings("unchecked")
Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes"));
List<String> list = new ArrayList<>();
for (String scope : scopes.keySet()) {
list.add(scope);
}
model.put("scopes",list);
Cookie[] cookies = request.getCookies();
boolean bool = Arrays.stream(cookies).anyMatch(x->x.getName().equals("remember-me-cookie-name"));
Principal principal = request.getUserPrincipal();
String usernmae = principal.getName();
model.put("username",usernmae);
String check = bool==true ? "true" : "false";
model.put("remember",check);
return "approval";
}
看一下我的具体的approval.html
视图
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
</head>
<body>
<form th:action="@{../oauth/authorize}" th:method="post">
<div th:each="scope:${scopes}">
<input th:type="hidden" th:name="${scope}" th:value="true"/>
</div>
<input th:type="hidden" th:name="user_oauth_approval" th:value="true"/>
</form>
<script src="../../js/jquery.min.js"></script>
<script src="../../js/jquery.cookie.js"></script>
<script th:inline="javascript">
window.onload=function(){
var username = [[${username}]];
var remember = [[${remember}]];
if(remember == "true"){
$.cookie('phone', username, { expires:7,path: '/' });
}else{
$.cookie('phone', username, { path: '/' });
}
document.forms[0].submit();
}
</script>
</body>
</html>
注意配置文件的更改,因为thymeleaf语法的限制,具体的原因大请大家参考我的另一篇文章 Spring Security模板引擎之异常信息
到了这个地方可能很多人以为就结束了,其实还没有,当你注销的时候你会发现浏览器的cookie还有那个remember-me-cookie-name
,当你再次登录的时候它又会执行记住我功能的流程导致登录错误
解决注销登录
最开始很当然的想到的是清除cookie,这个地方我尝试了很久都没有成功,还被我们群里的一个只说不练的大神嘲笑(java是一门学问,简单的东西遇见了不同的情况,未必可以简单的对待),我希望有大神看见这篇文章过后能找到清除sping security oauth2
的记住我功能的cookie,最后谈一下我的解决方案,和以前处理token的思想类似,既然浏览器的cookie中保存了用户的一些信息,这些数据还要和数据库的存储的数据进行校验,既然cookie处理不了,那就转移中心去处理数据库的吧
看一下注销登录的代码
//....................
@FrameworkEndpoint
public class RevokeTokenEndpoint {
@Autowired
@Qualifier("consumerTokenServices")
ConsumerTokenServices consumerTokenServices;
private static final Logger logger = LoggerFactory.getLogger(RevokeTokenEndpoint.class);
@DeleteMapping("/oauth/exit")
@ResponseBody
public JSONObject revokeToken(String principal) {
//消除token
String access_token = JdbcOperateUtils.query(principal);
if (!access_token.equals("gzw")) {
if (consumerTokenServices.revokeToken(access_token)) {
logger.info("oauth2 logout success with principal: "+ principal);
//消除cookie的校验数据
JdbcOperateUtils.exit(principal);
return ResultUtil.toJSONString(ResultEnum.SUCCESS,principal);
}
}else {
logger.info("oauth2 logout fail with principal: "+ principal);
return ResultUtil.toJSONString(ResultEnum.FAIL,principal);
}
return ResultUtil.toJSONString(ResultEnum.UNKONW_ERROR,principal);
}
}
消除cookie的校验数据代码如下:
/** * 查询用户是否是用记住我登录 * @param username * @return */
public static void exit(String username) {
Connection connection = ConnectionUtils.getConn();
String sql1 = "SELECT series FROM persistent_logins WHERE username = ? limit 1";
String sql2 = "DELETE FROM persistent_logins WHERE username = ? ";
PreparedStatement preparedStatement1 = null;
PreparedStatement preparedStatement2 = null;
try {
preparedStatement1 = connection.prepareStatement(sql1);
preparedStatement1.setString(1, username);
ResultSet resultSet = preparedStatement1.executeQuery();
if (resultSet.next()){
preparedStatement2 = connection.prepareStatement(sql2);
preparedStatement2.setString(1, username);
preparedStatement2.execute();
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
ConnectionUtils.releaseConnection(connection);
}
}
最后附上建表语句,创建persistent_logins
表create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
总结:
希望这篇文章对大家在日常工作中能够有所帮助,我也会在平时工作中把遇见的相关问题继续更新到文章中。后续我会将代码同步到github上供大家一起学习,希望大家给出中肯的意见。
参考本人github地址:https://github.com/dqqzj/spring4all/tree/master/oauth2