nginx配置sso登錄
下面的例子是如何在nginx配置sso登錄服務(wù)挫剑。
背景
用到幾個(gè)主要元素:應(yīng)用服務(wù)器(myweb)樊破,ngingx服務(wù)器,和認(rèn)證服務(wù)器(mycas)奔滑。
為了驗(yàn)證的簡(jiǎn)化朋其,所有的服務(wù)器都搭建在一臺(tái)主機(jī)上(假設(shè)當(dāng)前機(jī)器名為host.example.com),主機(jī)名即域名氓辣,三個(gè)服務(wù)通過(guò)三個(gè)不同的端口提供服務(wù)钞啸。
- 應(yīng)用服務(wù)器喇潘,監(jiān)聽(tīng)在主機(jī)端口4000颖低。
- CAS服務(wù)器,監(jiān)聽(tīng)在主機(jī)端口3000源武。
- nginx服務(wù)器粱栖,監(jiān)聽(tīng)在主機(jī)端口80脏毯。
sso登錄驗(yàn)證的基本流程
- 用戶通過(guò)服務(wù)器端口80來(lái)請(qǐng)求訪問(wèn)資源食店。
端口80實(shí)際上是nginx的端口,真正的應(yīng)用端口是4000价认;nginx會(huì)把請(qǐng)求最終路由到應(yīng)用服務(wù)器端口4000上用踩。
- 用戶通過(guò)服務(wù)器端口80來(lái)請(qǐng)求訪問(wèn)資源食店。
-
- nginx通過(guò)配置文件的auth_request模塊脐彩,把請(qǐng)求轉(zhuǎn)向CAS服務(wù)器進(jìn)行認(rèn)證(假設(shè)http://host.example.com:3000/auth)
- 2.1. 如果用戶沒(méi)有在CAS服務(wù)器上經(jīng)過(guò)登錄認(rèn)證姊扔,那么認(rèn)證服務(wù)器返回401錯(cuò)誤給nginx恰梢;nginx繼續(xù)根據(jù)配置文件對(duì)401錯(cuò)誤的處理方式证九,向用戶瀏覽器返回302狀態(tài)碼愧怜,并附帶一個(gè)認(rèn)證服務(wù)器的跳轉(zhuǎn)地址(http://host.example.com:3000/login)拥坛,然后客戶瀏覽器重新向認(rèn)證服務(wù)器發(fā)起登錄請(qǐng)求尘分。用戶在認(rèn)證服務(wù)器上進(jìn)行簡(jiǎn)單用戶名密碼驗(yàn)證:
- 2.1.1. 如果用戶為合法用戶:那么認(rèn)證服務(wù)器將生成session培愁,包含認(rèn)證過(guò)的用戶信息定续,以備將來(lái)的再次驗(yàn)證使用,并通過(guò)Set-cookie命令告知用戶瀏覽器摹察。用戶通過(guò)此Cookie即可獲得認(rèn)證服務(wù)器的認(rèn)可授權(quán)供嚎,此后用戶帶上此Cookie來(lái)訪問(wèn)認(rèn)證服務(wù)器(http://host.example.com:3000/auth)時(shí)峭状,認(rèn)證服務(wù)器會(huì)返回200的狀態(tài)碼优床。
- 2.1.2. 如果用戶為非法用戶:那么認(rèn)證服務(wù)器將不會(huì)session羔巢,這樣用戶無(wú)法獲得認(rèn)可的Cookie罩阵,那么當(dāng)再次訪問(wèn)(http://host.example.com:3000/auth)時(shí)稿壁,會(huì)繼續(xù)得到401錯(cuò)誤的錯(cuò)誤碼傅是,請(qǐng)求再次認(rèn)證蕾羊。
- 2.2. 假設(shè)用戶已經(jīng)授權(quán)成功龟再,那么當(dāng)用戶訪問(wèn)服務(wù)器中的資源時(shí)利凑,由于認(rèn)證服務(wù)器(http://host.example.com:3000/auth)返回返回200狀態(tài)碼嫌术,服務(wù)器允許用戶繼續(xù)訪問(wèn)度气。
我們使用node/express來(lái)模擬應(yīng)用服務(wù)器和CAS認(rèn)證服務(wù)器磷籍。
步驟
- 安裝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
參考官方文檔院领,不細(xì)說(shuō)。
https://nodejs.org/en/
- 安裝express-generator
我們用它來(lái)生產(chǎn)node項(xiàng)目框架汪诉。
$ npm install express-generator -g
-
創(chuàng)建兩個(gè)node項(xiàng)目
4.1. 創(chuàng)建應(yīng)用服務(wù)器
$ express -e --git myweb
$ cd myweb
$ npm install
修改認(rèn)證服務(wù)器端口為4000
$ cat bin/www
var port = normalizePort(process.env.PORT || '4000');
4.2. 創(chuàng)建認(rèn)證服務(wù)器
$ express -e --git mycase
$ cd note
$ npm install
保持認(rèn)證服務(wù)器端口為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;
}
}
}
- 修改應(yīng)用服務(wù)器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;
- 修改認(rèn)證服務(wù)器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;
為認(rèn)證服務(wù)器添加一個(gè)登錄頁(yè)面:
$ 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>
- 啟動(dòng)應(yīng)用
啟動(dòng)認(rèn)證服務(wù)器
$ DEBUG=myweb:* npm start
啟動(dòng)應(yīng)用服務(wù)器
$ DEBUG=mycas:* npm start
啟動(dòng)nginx
$ sudo service nginx restart
nginx的錯(cuò)誤日志在:/var/log/nginx/error.log
另外如果碰到如下錯(cuò)誤:
(13: Permission denied) while connecting to upstream:[nginx]
請(qǐng)參考下面鏈接:
https://stackoverflow.com/questions/23948527/13-permission-denied-while-connecting-to-upstreamnginx
- 登錄訪問(wèn)
登錄
http://host.example.com
瀏覽器會(huì)跳轉(zhuǎn)到登錄頁(yè)面,http://host.example.com:3000/login于樟,輸入用戶名密碼(admin/admin)后迂曲,跳轉(zhuǎn)到應(yīng)用服務(wù)器頁(yè)面寥袭,顯示"Welcome Page!"
退出
http://host.example.com/logout
退出后,頁(yè)面重新跳轉(zhuǎn)到登錄頁(yè)面队寇。
- 關(guān)于應(yīng)用服務(wù)器如何獲取認(rèn)證服務(wù)器的數(shù)據(jù)
11.1 設(shè)置request.Headers
在認(rèn)證服務(wù)器章姓,認(rèn)證成功時(shí):
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;
}
從認(rèn)證服務(wù)器的upstream獲取x_forwarded_user和x_idcs_user啤覆,注意用小寫(xiě)窗声,然后設(shè)置到應(yīng)用服務(wù)器的request里面去笨觅。
11.2 設(shè)置瀏覽器cookie
在認(rèn)證服務(wù)器的post/login成功時(shí):
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的內(nèi)容了。