什么是单点登陆?
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO单点登录访问流程主要有以下步骤:
1. 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
2. 定向认证:SSO客户端会重定向用户请求到SSO服务器。
3. 用户认证:用户身份认证。
4. 发放票据:SSO服务器会产生一个随机的Service Ticket。
5. 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
6. 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。
CAS服务端部署
-
生成证书
castest是证书名称,F:/keys/castest为存 放位置
keytool -genkey -alias castest -keyalg RSA -keystore F:/keys/castest
-
导出证书
keytool -export -file F:/keys/castest.crt -alias castest -keystore F:/keys/castest
- 将证书导入到客户端JRE中(注意、是导入JRE中),如果security中已经存在cacerts,需要先将其删除。
keytool -import -keystore "D:\java\jdk\jre\lib\security\cacerts" -file F:/keys/castest.crt -alias castest
-
配置服务器端
1、从https://github.com/apereo/cas/releases上下载cas服务器端cas-server-4.0.0-release.zip,在modules目录下找到cas-server-webapp-4.0.0.war,将其复制到%TOMCAT_HOME%\webapps下,并将名称改为cas.war
2、修改%TOMCAT_HOME%\conf\server.xml文件
<Connector protocol="org.apache.coyote.http11.Http11Protocol" port="8443" minSpareThreads="5" maxSpareThreads="75" enableLookups="true" disableUploadTimeout="true" acceptCount="100" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="F:/keys/castest"<!—生成证书时的路径,证书名--> keystorePass="castest"<!—证书密码--> truststoreFile="D:/java/jdk/jre/lib/security/cacerts"<!-jre证书目录-> clientAuth="false" sslProtocol="TLS"/>
3、在C:\Windows\System32\drivers\etc\hosts里增加ip地址与域名之间的联系
127.0.0.1 sso.castest.com
4、测试、启动tomcat:https://localhost:8443/
https://localhost:8443/cas/login:cas登陆页面,没有配置数据库之前,账号密码相同即可登陆
CAS客户端应用
cas配置文件
package intellif.szwj.cas;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
@ConditionalOnProperty(name = "cas.login", havingValue = "true")
public class CasFilterConfig {
@Value("${cas.server.client.url}")
private String appUrl;
@Value("${cas.server.url}")
private String casUrl;
@Value("${cas.server.login.url}")
private String casServerLoginUrl;
/**
* 单点登录认证
* @return
*/
@Bean
public FilterRegistrationBean authenticationFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns("/wjcas/login");
registrationBean.setName("CAS Filter");
registrationBean.addInitParameter("casServerLoginUrl",casServerLoginUrl);
registrationBean.addInitParameter("serverName", appUrl );
registrationBean.setOrder(3);
return registrationBean;
}
/**
* 单点登录校验
* @return
*/
@Bean
public FilterRegistrationBean cas20ProxyReceivingTicketValidationFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
registrationBean.addUrlPatterns("/wjcas/login");
registrationBean.setName("CAS Validation Filter");
registrationBean.addInitParameter("casServerUrlPrefix", casUrl );
registrationBean.addInitParameter("serverName", appUrl );
registrationBean.setOrder(4);
return registrationBean;
}
/**
* 单点登录请求包装
* @return
*/
@Bean
public FilterRegistrationBean httpServletRequestWrapperFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new HttpServletRequestWrapperFilter());
registrationBean.addUrlPatterns("/wjcas/login");
registrationBean.setName("CAS HttpServletRequest Wrapper Filter");
registrationBean.setOrder(5);
return registrationBean;
}
/**
* 单点登录本地用户信息
* @return
*/
@Bean
public FilterRegistrationBean localRedirectFilterBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(localRedirectFilter());
registrationBean.addUrlPatterns("/wjcas/login");
registrationBean.setName("localRedirectFilter");
registrationBean.setOrder(6);
return registrationBean;
}
/**
* 单点登录本地用户信息
* @return
*/
@Bean
public FilterRegistrationBean localLoginFilterBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(localLoginFilter());
registrationBean.addUrlPatterns("/locallogin/login");
registrationBean.setName("localLoginFilter");
registrationBean.setOrder(7);
return registrationBean;
}
@Bean
public Filter localRedirectFilter() {
return new LocalRedirectFilter();
}
@Bean
public Filter localLoginFilter() {
return new LocalLoginFilter();
}
}
前端点击CAS登陆发送http://127.0.0.1:8083/api/wjcas/login请求
cas进行拦截跳转至cas.server.login.url
输入用户名、密码点击登陆进入重定向拦截器
package intellif.szwj.cas;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class LocalRedirectFilter implements Filter {
private static Logger logger = LogManager.getLogger(LocalRedirectFilter.class);
@Autowired
private JdbcTemplate jdbcTemplate;
@Value("${cas.client.redirect.url}")
private String redirectUrl;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("==========================应用系统拦截器开始============================");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// _const_cas_assertion_是CAS中存放登录用户名的session标志
Object object = httpRequest.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
if (object != null) {
String loginCode = httpRequest.getRemoteUser();
httpResponse.sendRedirect(redirectUrl + "?loginCode="+loginCode);
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
logger.info("==========================应用系统拦截器结束============================");
}
}
进入重定向地址http://127.0.0.1:8083/api/caslogin.html?loginCode=superuser
重新加载cas登陆页面,发送http://127.0.0.1:8083/api/locallogin/login请求
LocalLoginFilter拦截器进行拦截,先获取用户名,然后查询数据库,最后模拟登陆获取token值
package intellif.szwj.cas;
import intellif.utils.HttpUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LocalLoginFilter implements Filter {
private static Logger logger = LogManager.getLogger(LocalLoginFilter.class);
@Autowired
private JdbcTemplate jdbcTemplate;
@Value("${cas.client.token.url}")
private String tokenUrl;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("==========================应用系统拦截器开始============================");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String pathCode = request.getParameter("loginCode");
if (StringUtils.isNotEmpty(pathCode)) {
String result = "";
StringBuilder sb = new StringBuilder();
sb.append("select login,password from intellif_base.t_user where login_code = '").append(pathCode).append("'");
List<Map<String, Object>> userList = jdbcTemplate.queryForList(sb.toString());
if (userList.size() > 0) {
String login = userList.get(0).get("login").toString();
String password = userList.get(0).get("password").toString();
Map<String, String> headMap = new HashMap<>(16);
StringBuilder tokenRequest = new StringBuilder("password=").append(password);
tokenRequest.append("&username=").append(login);
tokenRequest.append("&grant_type=password&scope=read+write&client_secret=123456&client_id=clientapp");
logger.info(tokenRequest.toString());
headMap.put("Authorization", "Basic Y2xpZW50YXBwOjEyMzQ1Ng==");
headMap.put("Content-Type", "application/x-www-form-urlencoded");
StringEntity stringEntity = new StringEntity(tokenRequest.toString(), "UTF-8");
stringEntity.setContentType("application/json");
stringEntity.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
result = HttpUtils.doPost(tokenUrl, headMap, null, stringEntity);
if (request == null) {
result = "获取系统权限失败,请联系管理员";
}
} else {
result = "无此用户,请联系管理员";
}
response.setContentType("application/json; charset=utf-8");
response.setCharacterEncoding("UTF-8");
OutputStream out = response.getOutputStream();
out.write(result.getBytes("UTF-8"));
out.flush();
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
logger.info("==========================应用系统拦截器结束============================");
}
}
前端登陆页面
步骤:
1、CAS登陆按钮点击发送http://127.0.0.1:8083/api/wjcas/login请求;
2、返回带有loginCode的数据http://127.0.0.1:8083/api/caslogin.html?loginCode=superuser;
3、重新加载页面时如果loginCode不为空,则会发送http://127.0.0.1:8083/api/caslogin.html?loginCode=superuser请求,返回token;
4、将token放入前端session里,加载http://localhost:5000/;
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>login</title>
<style type="text/css">
*{
margin: 0;
padding: 0;
}
#wrap {
height: 719px;
width: 100;
background-repeat: no-repeat;
background-position: center center;
position: relative;
}
#head {
height: 120px;
width: 100;
background-color: #66CCCC;
text-align: center;
position: relative;
}
#wrap .logGet {
height: 408px;
width: 368px;
position: absolute;
background-color: #FFFFFF;
top: 10%;
right: 35%;
}
.logC button {
width: 100%;
height: 45px;
background-color: #40a9ff;
border: none;
color: white;
font-size: 18px;
}
#wrap .logGet .logC {
width: 86%;
margin-top: 0px;
margin-right: auto;
margin-bottom: 0px;
margin-left: auto;
}
.title {
font-family: "宋体";
color: #FFFFFF;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 使用css3的transform来实现 */
font-size: 36px;
height: 40px;
width: 50%;
}
</style>
</head>
<body>
<div class="header" id="head">
<div class="title">吴江公安分局动态人像识别系统</div>
</div>
<div class="wrap" id="wrap">
<div class="logGet">
<div class="logC">
<button onclick="doLoginPre()">CAS 登录</button></a>
</div>
</div>
</div>
<script>
var serverIp = "127.0.0.1";
var webUrl = "http://" + serverIp + ":8083/api/static/index.html";
var apiUrl = "http://" + serverIp + ":8083/api";
var casLogin = apiUrl + "/wjcas/login";
var casLogin2 = apiUrl + "/locallogin/login";
var getRightUrl = apiUrl + "/intellif/user/right/";
var languageListUrl = apiUrl + "/intellif/national/language/list";
window.onload=function(){
//先清除登录缓存
window.localStorage.clear();
var post_data = getRequest();
var loginCode = post_data['loginCode'];
if (!loginCode) {
return;
}
// Check browser support
if (typeof(Storage) !== "undefined") {
try {
doAjax(casLogin2+"?loginCode="+loginCode,"GET",null,function(res) {
if (isError(res)) {
alert("CAS login error!" + res);
return;
}
var resObj = JSON.parse(res);
var token = resObj.access_token;
document.cookie="token="+token;
var userInfo = resObj;
var userInfoName = userInfo.oauth_AIK_user_info.login;
doAjax(getRightUrl+userInfoName,"GET",null,function(res1) {
if (isError(res1)) {
alert("PKI login error!" + res1);
window.location=webUrl;
return;
}
var res1Obj = JSON.parse(res1);
userInfo.oauth_AIK_user_info = res1Obj.data.userinfo;
userInfo.oauth_AIKK_role_info_s = res1Obj.data.roleInfoList;
userInfo.resIds = res1Obj.data.roleInfoList[0].resIds;
//将获取到的用户信息存储在本地的localstorage中!
window.localStorage.setItem(
'userInfo',
JSON.stringify(userInfo)
);
//兼容深目1.x存储的oauth
window.localStorage.setItem('oauth', JSON.stringify(userInfo));
window.localStorage.setItem(
'currentSessionTime',
new Date().getTime()
);
if (res1Obj.data.userinfo.locale) {
window.localStorage.setItem(
'userLocale',
JSON.stringify(res1Obj.data.userinfo.locale)
);
}
doAjax(languageListUrl,"GET",null,function(res2) {
if (isError(res2)) {
alert("PKI login error!" + res2);
window.location=webUrl;
return;
}
var res2Obj = JSON.parse(res2);
// 本地存储可支持的语言列表
window.localStorage.setItem(
'languageList',
JSON.stringify({ languageList: res2Obj.data })
);
window.location=webUrl;
}, token);
}, token);
});
} catch (e) {
alert("CAS登录失败:" + e);
}
} else {
document.getElementById("result").innerHTML = "sorry!your Browsers do not support Web Storage ...";
}
}
function doLoginPre() {
window.location=casLogin;
}
//获得URL后面带的参数,并封装成对象
function getRequest() {
var request = new Object();
//获得URL问号后面的字符串,包括问号
var param = window.location.search;
if (param != '') {
//去掉问号
param = param.substring(1);
//用‘&’,分离出带有的参数
var params = param.split("&");
//用‘=’,循环将参数分离成key-value形式,并封装到对象中
for (var i = 0; i < params.length; i++) {
var kv = params[i].split("=");
request[kv[0]] = kv[1];
}
}
return request;
}
function doAjax(url, method, data, callback, token) {
var xmlhttp;
if (window.XMLHttpRequest)
{
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp=new XMLHttpRequest();
}
else
{
// IE6, IE5 浏览器执行代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.open(method,url,true);
xmlhttp.setRequestHeader("Content-type","application/json; charset=utf-8");
if (token) {
xmlhttp.setRequestHeader("Authorization","Bearer " + token);
}
if (method == "POST") {
xmlhttp.send(JSON.stringify(data));
} else {
xmlhttp.send();
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4)
{ if (xmlhttp.status==200) {
if (callback) {
callback(xmlhttp.responseText);
}
} else {
callback("error");
}
}
}
}
function isError(jsonStr) {
try {
var obj = JSON.parse(jsonStr);
if (obj['errCode'] && obj['errCode'] != '0') {
return true;
} else {
return false;
}
} catch (e) {
return true;
}
}
</script>
</body>