简介:文章主要介绍企业多个应用系统的统一认证授权系统的实现,提供完整的用户身份认证以及权限管理,同一用户权限管理,实现所有业务子系统的单点登录登出。
1.采用框架
spring3.1.1 + springmvc + mybatis3.1.1 + shiro1.2.4 + activemq 前端框架:bootstrap
shiro:权限控制框架
activemq:授权中心token分发以及日志消息处理
2.系统主要功能
2.1业务系统:管理需要被授权的子系统(主要包括:系统名称,系统首页地址,系统授权码)
2.2 模块管理:维护系统的功能模块
2.3 菜单管理:维护系统的菜单
2.4 按钮管理:按钮维护(系统权限精确到按钮级)
2.5 用户管理:用户信息维护
2.6 角色管理:角色维护
主要表结构如下:
授权主要流程图如下:
3.授权中心登录实现
系统的权限管理采用shiro框架控制(shiro教程:http://jinnianshilongnian.iteye.com/blog/2018398)
登录及token分发主要代码:
@RequestMapping("/login")
public String login(String account,String password,ModelMap model,HttpServletRequest request,HttpServletResponse response){
log.info("用户登录*******************");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(account,MD5Encrypt.Md5(password));
token.setRememberMe(false);
try {
subject.login(token);
//登录成功后获取重定向地址 该地址为业务系统传递的登录成功后跳转返回地址
Object obj = request.getSession().getAttribute("redictUrl");
String url = obj == null ? null : obj.toString();
if(url != null && url.length() > 0){
//当重定向地址不为空时 重定向到当前URL且带上token
url += "?token=" + request.getSession().getAttribute("token") + "&";
request.getSession().setAttribute("redictUrl",null);
return "redirect:" + url;
}
//重定向地址为空时 跳转到授权中心主页
return "redirect:page/main.jsp";
}catch (AuthenticationException ex) {
log.info(("登录失败错误信息:"+ex.getMessage()));
ex.printStackTrace();
token.clear();
//设置错误信息 跳转到登录页面
model.addAttribute("errorMsg",ex.getMessage());
return "login/login";
}
}
/**
* 认证信息 通过比对数据库要比对用户和密码
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken paramAuthenticationToken)
throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) paramAuthenticationToken;
String userName = token.getUsername();
if (userName != null && !"".equals(userName)) {
// 用户登录
SystemUserVo user = systemUserService.userLogin(userName,
new String(token.getPassword()));
if (user == null) {
log.info("用户名【{}】或密码【{}】错误", userName, new String(token.getPassword()));
throw new AuthenticationException("用户名或密码错误");
}
//用户状态为空或者为0时表示该帐号被禁用
if (user.getStatus() == null || user.getStatus() == 0) {
log.info("用户名【{}】被禁用,登录失败", userName);
throw new AuthenticationException("该帐号已被禁用");
}
// 用户登录成功后 生成token并下发业务系统
JSONObject tokenJson = getTokenJson(user);
// 将token放入缓存中
CacheUtils.addCache(tokenJson.get("token"), tokenJson);
//将用户信息放入session中
SecurityUtils.getSubject().getSession().setAttribute("currentUser",JSONObject.fromObject(tokenJson.get("userInfo")));
//将token放入session中
SecurityUtils.getSubject().getSession().setAttribute("token", tokenJson.get("token"));
//将token信息放入session中
SecurityUtils.getSubject().getSession().setAttribute("tokenJson", tokenJson);
// MQ下发token(主题订阅方式) MQUtils.getQueueName("mqTokenTopic")获取token订阅的主题
TopicServer.getSingleInstance().publishMessageThread(MQUtils.getQueueName("mqTokenTopic"), tokenJson.toString());
// 清除当前用户之前登录的token 用于禁止一个帐号多次登录
String removeToken = CacheUtils.clearToken(tokenJson);
// MQ清除token
Map
map.put("type", "delete");
map.put("token", removeToken);
//告知子系统当前tonken已被删除
TopicServer.getSingleInstance().publishMessageThread(
MQUtils.getQueueName("mqTokenTopic"), JSONObject.fromObject(map).toString());
// token生成和下发完成
return new SimpleAuthenticationInfo(user.getAccount(), user
.getPassword().toCharArray(), getName());
} else {
log.info("用户名为空");
}
return null;
}
生成token(包含用户权限信息,此处可优化为只下发token,业务系统利用用户登录token和系统授权token获取用户本系统的角色及菜单权限)
/**
*
* @Description: TODO 根据当前登录用户 生成token
* @param @param user 当前登录用户
* @param @return
* @return JSONObject token及授权信息的json字符串
* @throws
* @author 邱林
* @date 2015年11月2日 下午3:00:33
*/
private JSONObject getTokenJson(SystemUserVo user) {
Map
Long userId = user.getId();
// 生成token并和客户端绑定
tokenMap.put("type", "add");// 设置tokne的类型为新加token
tokenMap.put("times", 0);//设置token被激活的次数为0
String token = TokenUtils.getToken();//获取token
tokenMap.put("token", token);//设置token
tokenMap.put("clientIp", SecurityUtils.getSubject().getSession()
.getHost());//设置当前token生成时的IP
tokenMap.put("userInfo", user);// 设置用户信息
tokenMap.put("role", systemPressService.getUserRole(userId));// 设置用户角色
List
4.业务系统从授权中心获取用户授权
业务系统在系统启动时订阅tokenTopic 用于接收授权中心分发的token,代码如下:
//订阅token主题 TokenSubMessageHandler订阅消息的消息处理类
TopicClient tc = new TopicClient(new TokenSubMessageHandler());
try {
//MQUtils.getTokenTopic() 获取token订阅的主题
tc.subTopic(MQUtils.getTokenTopic());
} catch (JMSException e) {
e.printStackTrace();
}
业务系统shiro登录地址配置:(http://qiulinq.eicp.net:801为授权中心地址,redictUrl=http://qiulinq.eicp.net:802/log为登录成功后跳转到对应业务系统的链接)
授权中心http://qiulinq.eicp.net:801/logout实现如下:
@RequestMapping("/logout")
public String logout(HttpServletRequest request){
//调用shiro的登出方法
SecurityUtils.getSubject().logout();
//获取重定向地址
String redictUrl = request.getParameter("redictUrl");
if(redictUrl != null && redictUrl.length() > 0){
//当重定向地址不为空时 放到session中
request.getSession().setAttribute("redictUrl", redictUrl);
}
//获取当前session中的token
Object token = request.getSession().getAttribute("token");
if(token != null){
Map
map.put("type", "delete");
map.put("token", token);
//告知业务端token失效
TopicServer.getSingleInstance().publishMessageThread(MQUtils.getQueueName("mqTokenTopic"), JSONObject.fromObject(map).toString());
}
return "login/login";
}
授权中心登录成功后会携带token跳转回到当前业务子系统。
5.子系统过滤及token验证
在shiro的配置文件中新增单点登录的过滤器:
shiro主要过滤配置:
/** = sso
SSOFilter代码如下:
package com.shuilin.support.filter;
import java.util.UUID;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import net.sf.json.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.shuilin.support.cache.CacheUtils;
import com.shuilin.support.utils.MD5Encrypt;
/**
*
* ClassName: SSOFilter 单点登录过滤器 继承shiro的认证过滤器
* @Description: TODO
* @author 邱林
* @date 2015年11月3日 上午9:20:20
*/
public class SSOFilter extends AuthorizationFilter{
private static Logger log = LoggerFactory.getLogger(SSOFilter.class);
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object obj) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getParameter("token");
//判断当前session中是否存在用户
out : if(req.getSession().getAttribute("currentUser") != null){
//存在用户 则取当前session中的tokenJson对象
JSONObject tokenJson = (JSONObject) req.getSession().getAttribute("tokenJson");
if(tokenJson != null){
//当tokenJson不为null时 判断当前token是否在token缓存中存在 判断该token还有效
if(CacheUtils.getCache(tokenJson.getString("token")) == null){
log.info("token失效,强制用户退出..............");
//当token缓存中取到的值为空时说名当前token已被移除缓存 token已失效
SecurityUtils.getSubject().logout();
//判断当前请求(request)中是否包含token 不包含则直接return false表示验证失败
if(token == null){
return false;
}else{
//包含token则跳出out循环 继续验证当前request的token是否有效
break out;
}
}
}
return true;
}
//当token不为null时做登录验证
if(token != null && token.length() > 0){
try{
//获取shiro的主体
Subject subject = SecurityUtils.getSubject();
//创建shiro的token token作为用户名 密码为随机的uuid(此处只对token做验证 密码无实际意义)
UsernamePasswordToken subToken = new UsernamePasswordToken(token,MD5Encrypt.Md5(UUID.randomUUID().toString()));
//shiro做登录
subject.login(subToken);
//登录成功时返回true
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}
}
log.info("用户未登录,且不存在token或者token错误,重定向到授权中心登录,token:{}",token);
return false;
}
}
客户端系统的token验证以及获取用户权限代码:
package com.shuilin.support.filter;
import java.util.HashMap;
import java.util.Map;
import net.sf.json.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.shuilin.support.cache.CacheUtils;
import com.shuilin.support.mq.client.MessageSender;
import com.shuilin.support.mq.util.MQUtils;
import com.shuilin.support.utils.HttpClientUtil;
import com.shuilin.support.utils.PropertiesUtils;
import com.shuilin.support.utils.SessionUtils;
public class ShiroRealm extends AuthorizingRealm{
private static Logger log = LoggerFactory.getLogger(ShiroRealm.class);
/**
* 授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection paramPrincipalCollection) {
return SessionUtils.getUserPress();
}
/**
* 认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken paramAuthenticationToken)
throws AuthenticationException {
//用户token
UsernamePasswordToken userToken = (UsernamePasswordToken) paramAuthenticationToken;
//获取到授权中心下发的token
String token = userToken.getUsername();
Subject subject = SecurityUtils.getSubject();
//校验token
if(checkToken(token, subject.getSession().getHost())){
return new SimpleAuthenticationInfo(token, userToken.getPassword(), getName());
}
return null;
}
private static boolean checkToken(String token,String clientIp){
if(token == null){
log.info("单点登录时获取到的token为空,直接返回false【单点登录失败】");
return false;
}
log.info("单点登录时获取到的token为:{}",token);
//取得缓存中的token
JSONObject tokenJson = (JSONObject) CacheUtils.getCache(token);
if(tokenJson == null){
log.info("单点登录时缓存中获取token信息为空,token为:{}",token);
String url = PropertiesUtils.readProperties("authriazeUrl", "com/shuilin/config/properties/server");
url += "checkToken?token=" + token;
log.info("通过HTTP请求实时校验token:{}",url);
String res = HttpClientUtil.sendGetRequest(url, "UTF-8");
log.info("HTTP请求校验token返回的结果为:{}",res);
//当http请求的结果为空时 说明当前token无效 直接返回
if(res == null || res.length() == 0){
return false;
}
tokenJson = JSONObject.fromObject(res);
//将http请求到的token信息放入到缓存中 防止filter校验时取不到
CacheUtils.addCache(tokenJson.get("token"), tokenJson);
}
log.info("单点登录时获取到的token信息为:{}" ,tokenJson);
//判断当前用户的IP与token绑定的IP是否一致
if(!clientIp.equals(tokenJson.getString("clientIp"))){
log.info("当前绑定的IP与token的IP不一致");
return false;
}
//MQ告知授权中心该系统激活了token
Map
map.put("token", token);
//inuse表示当前token被使用
map.put("type", "inUse");
//MQ发送消息 告知授权中心当前token被激活了一次 用户实现单点登录
MessageSender.getSingleInstance().sendMessageThread(JSONObject.fromObject(map).toString(), MQUtils.getQueueName("mqTokenServerQueue"));
//在tokenJson中获取到用户信息 并放入session中
Map
SecurityUtils.getSubject().getSession().setAttribute("currentUser", userMap);
//将当前token信息放入到session中
SecurityUtils.getSubject().getSession().setAttribute("tokenJson", tokenJson);
//return true表示当前的token通过校验
return true;
}
}
子系统根据系统token获取用户当前系统的菜单权限:
/**
*
* @Description: TODO获取用户的菜单权限
* @param @return
* @return Map
* @throws
* @author 邱林
* @date 2015年11月6日 上午10:20:39
*/
@RequestMapping("/menu")
@ResponseBody
public Map
//获取当前用户的授权信息
JSONObject json = (JSONObject) SecurityUtils.getSubject().getSession().getAttribute("tokenJson");
Map
//根据当前的系统授权码获取用户的菜单权限
String serverToken = PropertiesUtils.readProperties("serverToken", "com/shuilin/config/properties/server");
if(!json.getJSONObject("module").has(serverToken)){
log.info("无任何菜单的授权");
return null;
}
map.put("menu", json.getJSONObject("module").getString(serverToken));
return map;
}
AOP日志管理以及权限权限控制:
package com.shuilin.support.aop;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Resource;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.subject.Subject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import com.shuilin.support.bean.SystemOptLog;
import com.shuilin.support.dao.CommonDao;
import com.shuilin.support.mq.client.MessageSender;
import com.shuilin.support.mq.util.MQUtils;
import com.shuilin.support.utils.DateUtils;
import com.shuilin.support.utils.JsonUtils;
import com.shuilin.support.utils.SessionUtils;
@Component
@Aspect
public class AopLogUtils {
@Resource
private CommonDao commonDao;
// 处理并发日志
static ThreadLocal> map0 = new ThreadLocal
>() {
@Override
protected List
return new ArrayList
}
};
// log4j日志类
private static Logger log = LoggerFactory.getLogger(AopLogUtils.class);
// 定义service层log切入点
@Pointcut("execution(* com..service.impl..*.*(..))")
public void serviceLogPointCut() {}
// 定义dao层log切入点
@Pointcut("execution(* com..dao.impl..*.*(..))")
public void daoLogPointCut() {}
// controller的切面 用户强制验证按钮权限 com..controller
@Pointcut("within(@org.springframework.stereotype.Controller *)")
public void cutController() {}
// 发生异常的AOP拦截
public void doThrowing(JoinPoint jp, Throwable ex) {
ex.printStackTrace();
}
// dao层操作前日志
@Before("daoLogPointCut()")
public void doDaoLogBefore(JoinPoint joinPoint) {
String daoMethod = joinPoint.getSignature().getName();
// 只对增删改做日志记录
if ((daoMethod.startsWith("save") || daoMethod.startsWith("add")
|| daoMethod.startsWith("update") || daoMethod
.startsWith("del"))) {
List
if (list.size() > 0) {
SystemOptLog systemLog = new SystemOptLog();
BeanUtils.copyProperties(list.get(0), systemLog);
systemLog.setDaoClass(joinPoint.getTarget().getClass()
.toString());// 设置dao的类
systemLog.setDaoMethod(joinPoint.getSignature().getName());// 设置dao的方法
systemLog.setDaoParams(JSONArray
.fromObject(joinPoint.getArgs()).toString());// 设置dao的参数
Object obj = null;
Object params = joinPoint.getArgs()[0];
Class paramsClass = params.getClass();
if (!systemLog.getDaoMethod().startsWith("del")) {// 判断是否是删除
systemLog.setNewData(JSONArray.fromObject(
joinPoint.getArgs()).toString());// 设置数据操作后的值
try {
// 删除
obj = paramsClass.getDeclaredMethod("getId").invoke(
params);
} catch (Exception e) {
e.printStackTrace();
}
} else {
obj = joinPoint.getArgs()[0];
}
if (obj != null) {
String sql = "select * from "
+ changeToTableName(joinPoint.getTarget()
.getClass().toString()) + " where id = "
+ obj;
List
系统源码会在整理后贴出