目录
一:spring security整合spring session实现session共享
二:代码
1.springsecurity的配置文件
2.自定义的session管理
三:获取到当前的所有用户以及踢出一个用户
四:用户锁定
1.我们先来看看锁定的时序图
2.流程解析
3.代码
(1)登录流程验证验证
(2)登录失败加锁
五:安全权限的总结
1.为什么要用Spring security
2.为什么要大量自定义和重写security的框架方法
3.主要的技术要点
4.主要实现功能
其实这边坑还是还是很多了,我照着官网整合了半天,发现记住我的功能用不了(真的是springboot的几个版本都是用不了的)。然后就想跳过这个功能,毕竟之前也弄到过Redis上去了。那就去实现基于Redis的Session共享吧,结果整出来发现官网上一个说明心态崩了。
我这边也尝试了用spring session整合来实现session共享,但是不知道为何很多Bug无法解决。比如记住我功能的无法实现,因为每一次都需要重写Cookie的机制很不好弄然后session共享也没办法获取到所有的用户等。最后,只能重写speing security的方法了具体实现如下:主要是就是Session共享。(完整代码块在最后)
package com.config.Seurity;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.stereotype.Repository;
import com.config.Seurity.hander.AjaxSuccessHander;
import com.config.Seurity.hander.AjaxfailHander;
import com.config.Seurity.permission.CustomPermissionEvaluator;
import com.config.Seurity.pwdEnder.MyPasswordEncoder;
import com.config.Seurity.repository.MyPersistentTokenRepository;
import com.config.Seurity.repository.MySessionRegistryImpl;
import com.config.Seurity.service.LoginService;
/**
* spring security的配置
* ClassName: SecurityConfig
* Function: 一句话描述功能.
* auth: monxz
* date: 2019年8月28日 上午10:04:50
* @param
*
*
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private LoginService loginService;
//静态文件夹忽略
private String[] allowedRes= {"/static/**","/css/**","/js/**","/my/**","/img/**","/ajax/**","favicon.ico"};
//不需要验证的
private String[] allowedUrl= {"/api/**","/user/user/current"};
//登录执行的逻辑
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginService).passwordEncoder(new MyPasswordEncoder());
}
//配置信息
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置访问权限
http.authorizeRequests()
//允许匿名访问(如api)
.antMatchers(allowedUrl)
.permitAll()
//其他地址的访问均需验证权限
.anyRequest()
.authenticated();
//配置登录以及成功失败的处理方式
http.formLogin()
//指定登录页是"/view/login"
.loginPage("/view/login").permitAll() //
//ajax方式登录
.successHandler(new AjaxSuccessHander())
.failureHandler(new AjaxfailHander())
.loginProcessingUrl("/login")
.usernameParameter("username") //ajax请求必须的
.passwordParameter("password");
//form表单登录
// .defaultSuccessUrl("/view/index") //登录成功后默认跳转到路径"
//注销 ,直接访问 ip:port/logout
http .logout()
.logoutSuccessUrl("/view/login") //退出登录后跳转到登录主界面"
.deleteCookies() //有记住我功能,删除cookie
.permitAll();
//记住我
http.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60*15)
;
//跨域以及其他的一些配置
http .csrf()
.disable() // 关闭CSRF跨域
.headers()
.frameOptions()
.sameOrigin(); // 允许加载frame子菜单
http.sessionManagement()
// .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.sessionFixation()
.migrateSession()
.invalidSessionUrl("/view/login")
.maximumSessions(1)
.expiredUrl("/view/login")
.sessionRegistry(sessionRegistry())
;
}
//静态资源
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers(allowedRes);
}
//================================权限认证=======================================
// 注入自定义url和权限验证器
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
//==========================session管理====================================================
//==========================session管理====================================================
//session全局监听
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SessionRegistry sessionRegistry() {
//自定义session管理
return new SessionRegistryImpl();
}
//===================记住我功能的实现===============================
//记住我功能的Token存储在Redis
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new MyPersistentTokenRepository();
}
}
package com.config.Seurity.repository;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.alibaba.fastjson.JSONObject;
import com.config.redis.constant.redisConstant;
import com.config.redis.service.redisService;
import commons.json.JSON;
import commons.utils.DateUtils;
@Component
public class MySessionRegistryImpl implements SessionRegistry,
ApplicationListener{
protected final Log logger = LogFactory.getLog(MySessionRegistryImpl.class);
//====================================================
@Autowired
private redisService redisService;
private String hash_key=redisConstant.security_session_principals_key;
private long timeout=redisConstant.security_session_timeout;
//=====================================================
public void sessionIdsPut(String sessionId,SessionInformation t) {
String str=JSON.marshal(t);
redisService.set(sessionId, str,timeout);
}
public void sessionIdsRemove(String sessionId) {
redisService.remove(sessionId);
}
public Set principalsGet(Object principal){
String str=JSON.marshal(principal);
return (Set) redisService.hmGet(hash_key, str);
}
public Set principalsPutIfAbsent(Object principal,Set strs){
Set res=new HashSet();
if(principalsGet(principal) != null ) {
res= principalsGet(principal);
}
String str=JSON.marshal(principal);
redisService.hmSet(hash_key, str, strs,timeout);
return res;
}
public void principalsRemove(Object principal){
String str=JSON.marshal(principal);
redisService.remove(str);
}
//===================================================
@Override
public SessionInformation getSessionInformation(String sessionId) {
String str = redisService.get(sessionId);
try {
if(str!= null && str.trim().length() != 0) {
SessionInformation sf = jsonToEntity(str);
return sf;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public List
package com.moudle.user.service.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.catalina.mbeans.UserMBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Service;
import com.config.Seurity.model.CurrentUser;
import com.moudle.user.service.SessionService;
import commons.utils.DateUtils;
/**
*
* ClassName:SessionServiceImpl
* Reason: TODO ADD REASON.
* Date: 2019年9月3日 下午3:25:32
* @author Owner
* @version
* @since JDK 1.8
* @see
*/
@Service
public class SessionServiceImpl implements SessionService{
@Autowired
private SessionRegistry sessionRegistry;
//获取到所有的在线对象
private List getAllUser(){
List list=new ArrayList();
try {
list = sessionRegistry.getAllPrincipals();
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
@Override
public List getAllLoginUser() {
List objs=this.getAllUser();
return objs;
}
@Async
@Override
public String kickSessionByUser(String[] userNames) {
StringBuffer noKickOut=new StringBuffer();
List objs=this.getAllUser();
for(Object obj:objs) {
if(obj instanceof CurrentUser) {
CurrentUser cou=(CurrentUser) obj;
for(String userName:userNames) {
if(cou.getUsername().equals(userName)) {
// false代表不包含过期session
List sessionInfos=sessionRegistry.getAllSessions(cou, false);
if(sessionInfos != null && sessionInfos.size() > 0) {
for(SessionInformation sessionInformation:sessionInfos) {
sessionInformation.expireNow();
sessionRegistry.refreshLastRequest(sessionInformation.getSessionId());
// sessionRegistry.removeSessionInformation(sessionInformation.getSessionId());
}
}else {
noKickOut.append(cou.getUsername()+",");
}
}
}
}
}
return noKickOut.toString();
}
}
从上面可以看出,关于锁定的主要有2个:一个是在登录时验证用户是否被锁定,另一个是在登录失败记性加锁。
package com.config.Seurity.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.config.Seurity.exception.UserException;
import com.config.Seurity.model.CurrentUser;
import com.config.redis.constant.redisConstant;
import com.config.redis.service.redisService;
import com.moudle.system.dao.MenuDao;
import com.moudle.system.model.Menu;
import com.moudle.user.model.User;
import com.moudle.user.service.SessionService;
import com.moudle.user.service.UserService;
import commons.utils.DateUtils;
import commons.utils.IpUtils;
/*
* 自定义用户登录授权service
*/
@Service("userDetailsService")
public class LoginService implements UserDetailsService{
@Autowired
private UserService userService;
@Autowired
private MenuDao menuDao;
@Autowired
private redisService redisService;
@Autowired
private HttpServletRequest request;
//登陆错误的Redis前缀
private String errorPrex=redisConstant.error_login_key_pre;
//登陆错误的超时时间
private Long errorTimeOut=redisConstant.errot_login_timeout;
//单位时间内最大错误登陆次数
private Long errorCount=redisConstant.error_login_max_count;
@Override
public UserDetails loadUserByUsername(String username) throws UserException {
User user=new User();
user.setUserName(username);
List userList = userService.login(user);
//验证错误登陆次数
if(valiErrorLogin() != 0L ){
throw new UserException("当前账号已被锁定请于"+DateUtils.secondsToDateStr(valiErrorLogin())
+"后重试!");
}
if (userList == null || userList.isEmpty()) {
throw new UserException("该用户不存在!");
}
User currentUser=userList.get(0);
//添加权限
List permsList=new ArrayList();
permsList =currentUser.getPerms()==null?
new ArrayList():currentUser.getPerms();
if(currentUser.getRole().getRoleRange() == 0) {
//超级管理员
List
说明:只要这块就是调用service只能用spring的上下文获取,而不能用@Resource或者@Autowired来获取,主要就是
RedisTemplate的初始化的时机。其他也就是RedisTemplate实现自增的一个方法
package com.config.Seurity.hander;
import java.io.IOException;
import java.io.PrintWriter;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import com.config.Seurity.exception.UserException;
import com.config.redis.constant.redisConstant;
import com.config.redis.service.redisService;
import com.config.redis.service.impl.RedisServiceImpl;
import commons.json.JSON;
import commons.result.DataResult;
import commons.utils.IpUtils;
/**
* 登录失败的ajax
* ClassName: AjaxSuccessHander
* Function: 一句话描述功能.
* auth: monxz
* date: 2019年8月28日 下午3:58:29
*
*
*/
@Component
public class AjaxfailHander extends SimpleUrlAuthenticationFailureHandler implements ApplicationContextAware{
private static ApplicationContext applicationContext;
//redis存储登录错误的key的前缀
private String errorLoginpre=redisConstant.error_login_key_pre;
//redis存储的超时间
private Long timeout=redisConstant.errot_login_timeout;
//足底啊的错误次数
private Long maxCount=redisConstant.error_login_max_count;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
setErrorCount(request);
String message = e.getMessage();
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(JSON.marshal(DataResult.buildFail(message)));
}
public void setErrorCount(HttpServletRequest request){
redisService redisService= applicationContext.getBean(redisService.class);
String key=errorLoginpre+IpUtils.replace(request, null);
Integer nowCount=redisService.get(key);
if(nowCount < maxCount) {
redisService.incr(key, timeout);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
if(AjaxfailHander.applicationContext == null ){
AjaxfailHander.applicationContext=applicationContext;
}
}
}
代码模块:这里我直接把项目上传了当然,这项目还不完整:https://pan.baidu.com/s/1sneGpr2xWx1IRe1VxDzDIA
到这里安全权限框架的一些内容基本就结束了,下面就做一些总结:
答:最早接触的是Shiro,还是认为Shiro较Security要简单的。但是为什么还是用security,主要原因是Spring security可以无缝连
接Spirng。一开始学习Cloud在Oauth2鉴权那里就是用的Security+oauht2,因此这里充分折腾了一番。这里给出security+
aouth2的博客:oauth2认证详解,oauth2的授权+密码,oauth2的数据持久化。
答:应为在搭建这个项目时,就是为之前的Spring cloud来完成血肉的。后续这个项目一定是向Dubbo和Cloud方向走的。因此,目前项目的所有的模块一定是基于分布式的。因此,大量折腾共享数据到Redis上的。
这里主要的技术要点是Spring security+spring session 。但是说实话,spring session就是为了实现分布式情况下负载均衡下的session一致,也就是相当于客户端和服务端的中间件。之前也有折腾spring session来共享session等信息,但是折腾了大半天参考了官网上的大量信息,发现session的功能还是有缺陷,所以还是基于security+redis实现分布式session共享。
(1)记住我功能:存储Redis实现15分钟内无需重复登录
(2)登录锁定:ip锁定,3次错误登录导致24小时锁定
(3)权限检验:后端权限校验,针对每一个访问的检验
(4)按钮级别的显示:后端权限具体化每一个前端的按钮
(5)获取所有在线用户:动态获取所有的在线用户
(6)踢出用户:根据用户名踢出当前在线用户
(7)单用户登录:后一个用户登录会踢出同名的用户
(8)单点登录:Redis存储可以实现单点登录(后续完成!)
在踢出用户锁定用户登录时长,用户被踢出来的瞬间会报500页面,然后在返回登录界面