Spring Security CORS

这篇文章描述SpringSecurity的跨域认证,通过前后端分离进行登陆认证,跨域调用资源。适合开发阶段使用的方式。
环境准备

  • jdk1.7+
  • Gradle2.8
  • IntelliJ 15 or eclipse
  • Redis

创建项目

通过idea创建spring-security-cors顶层模块,cors-resource服务端模块,cors-ui前端模块.目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
│  build.gradle
│ settings.gradle
├─cors-resource
│ │ build.gradle
│ └─src
│ ├─main
│ │ ├─java
│ │ └─resources
│ └─test
│ ├─java
│ └─resources
└─cors-ui
│ build.gradle
└─src
├─main
│ ├─java
│ └─resources
└─test
├─java
└─resources

顶层模块settings.gradle配置子模块信息:

1
include "cors-resource", "cors-ui"

build.gradle配置通用信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
buildscript {
ext {
springBootVersion = '1.4.0.RELEASE'
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}

subprojects{
group 'com.cjoop'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'

sourceCompatibility = 1.7
targetCompatibility = 1.7
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
testCompile("org.springframework.boot:spring-boot-starter-test")
}
}

跨域认证配置

通过Spring Security4.1.x提供的跨域支持进行配置,这里的认证结果进行了简单处理,在认证成功后返回sessionId给客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.cjoop.cors.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import com.cjoop.cors.handler.AuthenticationFailureHandler;
import com.cjoop.cors.handler.AuthenticationSuccessHandler;
import com.cjoop.cors.handler.LogoutSuccessHandler;

@Configuration
@EnableWebSecurity
public class SpringSecurityAutoConfiguration extends WebSecurityConfigurerAdapter{
@Autowired
@Qualifier("authenticationSuccessHandler")
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
@Qualifier("authenticationFailureHandler")
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("logoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;

/**
* 设置表单登陆信息
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
String loginProcessingUrl = "/j_spring_security_check";
//判断当前运行模式(开发阶段)添加跨域认证允许
http.cors();//主要是这行代码完成了跨域认证的支持
http.csrf().disable()
.authorizeRequests()
.antMatchers("/index.html", "/index.js","/","/favicon.ico").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl(loginProcessingUrl)
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()
.and()
.logout().logoutSuccessHandler(logoutSuccessHandler)
.permitAll();
}
}

通过Spring MVC提供的跨域支持进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.cjoop.cors.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcAutoConfiguration extends WebMvcConfigurerAdapter{
@Override
public void addCorsMappings(CorsRegistry registry) {
//判断当前运行模式(开发阶段)添加跨域允许
registry.addMapping("/**")
.allowedHeaders("x-requested-with","x-auth-token","content-type")
.maxAge(3600)
.allowedOrigins("*")
.allowCredentials(true);
}
}

认证完成后我们的sessionId是被持久化到了redis中,每一次浏览器调用资源请求都会携带X-Auth-Token和X-Requested-With两个头信息。这里我们实现一个跨域的会话策略CorsHttpSessionStrategy,这个策略合并了CookieHttpSessionStrategy和HeaderHttpSessionStrategy,以便提供本地策略和跨域策略的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.cjoop.cors.session;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.session.Session;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.HeaderHttpSessionStrategy;
import org.springframework.session.web.http.MultiHttpSessionStrategy;

/**
* 合并HeaderHttpSessionStrategy和CookieHttpSessionStrategy2种策略,该策略仅作开发时候使用
* @author 陈均
*
*/
public class CorsHttpSessionStrategy implements MultiHttpSessionStrategy{
CookieHttpSessionStrategy cookieHttpSessionStrategy = new CookieHttpSessionStrategy();
HeaderHttpSessionStrategy headerHttpSessionStrategy = new HeaderHttpSessionStrategy();

@Override
public String getRequestedSessionId(HttpServletRequest request) {
String sessionId = headerHttpSessionStrategy.getRequestedSessionId(request);
if(null==sessionId){
sessionId = cookieHttpSessionStrategy.getRequestedSessionId(request);
}
return sessionId;
}

@Override
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
headerHttpSessionStrategy.onNewSession(session, request, response);
cookieHttpSessionStrategy.onNewSession(session, request, response);
}

@Override
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
headerHttpSessionStrategy.onInvalidateSession(request, response);
cookieHttpSessionStrategy.onInvalidateSession(request, response);
}

@Override
public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
return cookieHttpSessionStrategy.wrapRequest(request, response);
}

@Override
public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) {
return cookieHttpSessionStrategy.wrapResponse(request, response);
}
}

项目cors-resource(application.properties)提供一个账户信息user来进行登陆,服务端口设置为9000:

1
2
server.port=9000
security.user.password=password

前端代码的准备

cors-ui项目中创建首页内容index.html,用来展示后端请求的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html ng-app="spring-security">
<head>
<meta charset="UTF-8">
<title>首页</title>
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
<style type="text/css">
.container{
padding-top: 50px;
}
</style>
</head>
<body ng-controller="IndexController">
<div class="container">
<div class="row">
<h3>Spring Security 跨域调用</h3>
</div>
<div class="row">
<div class="col-md-12">
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="username" class="col-md-2 control-label">Username:</label>
<div class="col-md-6">
<input type="text" class="form-control" id="username" name="username" ng-model="credentials.username"/>
</div>
</div>
<div class="form-group">
<label for="password" class="col-md-2 control-label">Password:</label>
<div class="col-md-6">
<input type="password" class="form-control" id="password" name="password" ng-model="credentials.password"/>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<a class="btn btn-primary" ng-click="resource()">resource</a>
<a class="btn btn-primary" ng-click="login()">login</a>
<a class="btn btn-primary" ng-click="logout()">logout</a>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<span ng-bind="message" style="color:red;"></span>
</div>
<div class="row">
<h1>Greeting</h1>
<div>
<p>The ID is {{greeting.id}}</p>
<p>The content is {{greeting.content}}</p>
<p>The sessionid is {{greeting.sessionId}}</p>
<p>The username is {{greeting.user.username}}</p>
</div>
</div>
</div>

<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="//cdn.bootcss.com/angular.js/1.5.8/angular.min.js"></script>
<script src="//cdn.bootcss.com/angular.js/1.5.8/angular-cookies.min.js"></script>
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="index.js"></script>
</body>
</html>

index.js通过ajax认证获取sessionId保存到cookies中,这样在刷新页面以后也能够获取到sessionId,每一个资源访问的头信息中都会携带X-Auth-Token和X-Requested-With。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(function(angular,$){
'use strict';
window.contextPath = "http://localhost:9000";
angular.module('spring-security', ['ngCookies'])
.run(['$http', '$cookies', function($http, $cookies) {
$http.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
$http.defaults.headers.common['X-Auth-Token'] = $cookies.get('X-Auth-Token');
}])
.controller('IndexController', ['$scope','$http','$cookies',function($scope,$http,$cookies) {
$scope.credentials = {};
$scope.login = function(){
return $http.post(contextPath+"/j_spring_security_check",null,{
params: $scope.credentials
}).then(function (resp) {
var result = resp.data;
$scope.message = result.text;
if(result.code == 0){//save cookie
$cookies.put('X-Auth-Token',result.data);
$http.defaults.headers.common['X-Auth-Token'] = result.data;
}
});
}
$scope.logout = function() {
$http.post(contextPath+'/logout', {}).then(function(resp){
$scope.message = resp.data.text;
$scope.greeting = {};
});
}
$scope.resource = function(){
$http.get(contextPath+'/resource').then(function(resp) {
if(resp.data.code==-500){
$scope.message = resp.data.text;
}
$scope.greeting = resp.data;
});
}
}]);
})(angular,jQuery);

测试程序

启动redis服务,执行命令 gradle bootRun 运行cors-resource和cors-ui两个项目,访问地址http://localhost:9000 体验本地调用效果,访问地址http://localhost:8080/ 体验跨域调用效果。 ,输入用户信息进行登陆,然后调用resource服务,结果如图:

以上就是结合Spring Security进行前后端分离开发时候的调用思路。
Fork me on GitHub