nginx配置sso登录

nginx配置sso登录

下面的例子是如何在nginx配置sso登录服务。

背景

用到几个主要元素:应用服务器(myweb),ngingx服务器,和认证服务器(mycas)。

为了验证的简化,所有的服务器都搭建在一台主机上(假设当前机器名为host.example.com),主机名即域名,三个服务通过三个不同的端口提供服务。

  1. 应用服务器,监听在主机端口4000。
  2. CAS服务器,监听在主机端口3000。
  3. nginx服务器,监听在主机端口80。

sso登录验证的基本流程

    1. 用户通过服务器端口80来请求访问资源。
      端口80实际上是nginx的端口,真正的应用端口是4000;nginx会把请求最终路由到应用服务器端口4000上。
    1. 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认证服务器。

步骤

  1. 安装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
  1. 安装node

参考官方文档,不细说。
https://nodejs.org/en/

  1. 安装express-generator

我们用它来生产node项目框架。

$ npm install express-generator -g
  1. 创建两个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');
  1. 修改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;
        }
    }
}
  1. 修改应用服务器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;
  1. 修改认证服务器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>
  1. 启动应用

启动认证服务器

$ 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

  1. 登录访问

登录
http://host.example.com
浏览器会跳转到登录页面,http://host.example.com:3000/login,输入用户名密码(admin/admin)后,跳转到应用服务器页面,显示"Welcome Page!"

退出
http://host.example.com/logout
退出后,页面重新跳转到登录页面。

  1. 关于应用服务器如何获取认证服务器的数据

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的内容了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容