nginx配置sso登录
下面的例子是如何在nginx配置sso登录服务。
背景
用到几个主要元素:应用服务器(myweb),ngingx服务器,和认证服务器(mycas)。
为了验证的简化,所有的服务器都搭建在一台主机上(假设当前机器名为host.example.com),主机名即域名,三个服务通过三个不同的端口提供服务。
- 应用服务器,监听在主机端口4000。
- CAS服务器,监听在主机端口3000。
- nginx服务器,监听在主机端口80。
sso登录验证的基本流程
- 用户通过服务器端口80来请求访问资源。
端口80实际上是nginx的端口,真正的应用端口是4000;nginx会把请求最终路由到应用服务器端口4000上。
- 用户通过服务器端口80来请求访问资源。
-
- nginx通过配置文件的auth_request模块,把请求转向CAS服务器进行认证(假设http://host.example.com:3000/auth)
- 2.1. 如果用户没有在CAS服务器上经过登录认证,那么认证服务器返回401错误给nginx;nginx继续根据配置文件对401错误的处理方式,向用户浏览器返回302状态码,并附带一个认证服务器的跳转地址(http://host.example.com:3000/login),然后客户浏览器重新向认证服务器发起登录请求。用户在认证服务器上进行简单用户名密码验证:
- 2.1.1. 如果用户为合法用户:那么认证服务器将生成session,包含认证过的用户信息,以备将来的再次验证使用,并通过Set-cookie命令告知用户浏览器。用户通过此Cookie即可获得认证服务器的认可授权,此后用户带上此Cookie来访问认证服务器(http://host.example.com:3000/auth)时,认证服务器会返回200的状态码。
- 2.1.2. 如果用户为非法用户:那么认证服务器将不会session,这样用户无法获得认可的Cookie,那么当再次访问(http://host.example.com:3000/auth)时,会继续得到401错误的错误码,请求再次认证。
- 2.2. 假设用户已经授权成功,那么当用户访问服务器中的资源时,由于认证服务器(http://host.example.com:3000/auth)返回返回200状态码,服务器允许用户继续访问。
我们使用node/express来模拟应用服务器和CAS认证服务器。
步骤
- 安装nginx
$ cat /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/rhel/7/$basearch/
gpgcheck=0
enabled=1
$ sudo yum install nginx
- 安装node
参考官方文档,不细说。
https://nodejs.org/en/
- 安装express-generator
我们用它来生产node项目框架。
$ npm install express-generator -g
-
创建两个node项目
4.1. 创建应用服务器
$ express -e --git myweb
$ cd myweb
$ npm install
修改认证服务器端口为4000
$ cat bin/www
var port = normalizePort(process.env.PORT || '4000');
4.2. 创建认证服务器
$ express -e --git mycase
$ cd note
$ npm install
保持认证服务器端口为3000
$ cat bin/www
var port = normalizePort(process.env.PORT || '3000');
- 修改nginx配置文件
$ cat /etc/nginx/nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
server {
listen 80 ;
server_name host.example.com;
location / {
auth_request /auth;
error_page 401 = @error401;
auth_request_set $user $upstream_http_x_forwarded_user;
proxy_set_header X-Forwarded-User $user;
proxy_pass http://host.example.com:4000;
}
location /logout {
proxy_pass http://host.example.com:4000/logout;
}
location /auth {
internal;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_pass http://host.example.com:3000;
}
location @error401 {
add_header Set-Cookie "redirect=$scheme://$http_host$request_uri;Domain=.example.com;Path=/;Max-Age=3000";
return 302 http://host.example.com:3000/login;
}
}
}
- 修改应用服务器app.js
$ cat myweb/app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyparser = require('body-parser');
var expresssession = require('express-session');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: false }));
app.use(expresssession({
secret: 'mysecret',
resave: true,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 3
}
}));
app.get('/', function (req, res) {
console.log("/ session.id =", req.session.id);
console.log("/ session.user =", req.session.user);
console.log("/ headers.user =", req.headers["user"]);
console.log("/ cookies.user =", req.cookies["user"]);
user = req.cookies["user"]
if (user) {
req.session.user= user;
res.end('Welcome Page!');
} else {
console.error("401 Unauthorized");
res.end('401 Unauthorized');
}
});
app.get('/logout', function (req, res) {
console.log("/ session.id =", req.session.id);
console.log("/ session.user =", req.session.user);
console.log("/ headers.user =", req.headers["user"]);
req.session.destroy();
res.redirect('http://host.example.com:3000/logout');
});
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
- 修改认证服务器app.js
$ cat mycas/app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyparser = require('body-parser');
var expresssession = require('express-session');
var cookie = require('cookie-parser');
var crypto = require('crypto');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var userTokens = {};
var userDatabase = { 'admin': 'admin' };
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(cookie());
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: false }));
app.use(expresssession({
secret: 'mysecret',
resave: true,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 3
}
}));
app.get('/login', function (req, res) {
console.log("/login req.session.id = ", req.session.id);
console.log("/login req.headers = ", req.headers);
res.sendFile(path.join(__dirname, './public/templates', 'login.html'));
});
app.post('/login', function (req, res) {
var user = req.body.username;
var pass = req.body.password;
console.log("/login POST req.session.id=", req.session.id);
if (userDatabase.hasOwnProperty(user) && userDatabase[user] == pass) {
// when username/password is valid
var token = crypto.createHash('sha256').update(req.session.id).digest("hex")
//req.session.user= user;
//req.session.token= token;
res.cookie('user', user);
res.cookie('token', token);
userTokens[token] = user;
console.log("/login POST generate token[", token, "] for user [", user, "]");
res.redirect('http://host.example.com')
} else {
res.redirect('/login');
}
});
app.get('/auth', function (req, res) {
console.log("/auth session.id : " + req.session.id);
console.log("/auth session.user : " + req.session.user);
console.log("/auth session.token : " + req.session.token);
console.log("/auth cookie.user : " + req.cookies.user);
console.log("/auth cookie.token : " + req.cookies.token);
token = req.cookies.token;
user = req.cookies.user;
if (userTokens[token] && userTokens[token] == user) {
console.log("/auth return success");
res.setHeader("X-Forwarded-User", user);
res.setHeader("user", user);
res.end()
} else {
console.log("/auth return failure");
res.status(401);
res.end()
}
});
app.get('/logout', function (req, res) {
console.log("/logout req.session.id : " + req.session.id);
req.session.destroy();
res.clearCookie("user");
res.clearCookie("token");
res.redirect('/login');
});
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
为认证服务器添加一个登录页面:
$ cat mycas/public/templates/login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Insert title here</title>
<style type="text/css">
body{
margin: 100px 0px;
padding:0px;
text-align:center;
align:center;
}
input[type=text], input[type=password]{
width:20%;
padding:7px 10px;
margin: 8px 0;
display:inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
button{
background-color:#4CAF50;
width: 10%;
padding: 9px 5px;
margin:5px 0;
cursor:pointer;
border:none;
color:#ffffff;
}
button:hover{
opacity:0.8;
}
#un,#ps{
font-family:'Lato', sans-serif;
color: gray;
}
</style>
</head>
<body>
<div id="container">
<form action="/login" method="post">
<h2>Login Form</h2>
<label for="username" id="un">Username:</label>
<input type="text" name="username" id="username"><br/><br/>
<label for="password" id="ps">Password:</label>
<input type="password" name="password" id="password"><br/><br/>
<button type="submit" value="Login" id="submit">Login</button>
</form>
</div>
</body>
- 启动应用
启动认证服务器
$ DEBUG=myweb:* npm start
启动应用服务器
$ DEBUG=mycas:* npm start
启动nginx
$ sudo service nginx restart
nginx的错误日志在:/var/log/nginx/error.log
另外如果碰到如下错误:
(13: Permission denied) while connecting to upstream:[nginx]
请参考下面链接:
https://stackoverflow.com/questions/23948527/13-permission-denied-while-connecting-to-upstreamnginx
- 登录访问
登录
http://host.example.com
浏览器会跳转到登录页面,http://host.example.com:3000/login,输入用户名密码(admin/admin)后,跳转到应用服务器页面,显示"Welcome Page!"
退出
http://host.example.com/logout
退出后,页面重新跳转到登录页面。
- 关于应用服务器如何获取认证服务器的数据
11.1 设置request.Headers
在认证服务器,认证成功时:
app.get('/auth', function (req, res) {
if (userTokens[token] && userTokens[token] == user) {
res.setHeader("X-Forwarded-User", user);
res.setHeader("X-Idcs-User", user);
res.end()
}
然后下nginx的配置文件里:
location / {
auth_request_set $user $upstream_http_x_forwarded_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $idcsuser $upstream_http_x_idcs_user;
proxy_set_header X-Idcs-User $idcsuser;
}
从认证服务器的upstream获取x_forwarded_user和x_idcs_user,注意用小写,然后设置到应用服务器的request里面去。
11.2 设置浏览器cookie
在认证服务器的post/login成功时:
app.post('/login', function (req, res) {
var user = req.body.username;
var pass = req.body.password;
console.log("/login POST req.session.id=", req.session.id);
if (userDatabase.hasOwnProperty(user) && userDatabase[user] == pass) {
res.cookie('user', user);
res.cookie('token', token);
}
}
这样浏览器就能看到cookie的内容了。