【Java杂货铺】用Security做权限极简入门
原来大多数单体项目都是用的shiro,随着分布式的逐渐普及以及与Spring的天生自然的结合。Spring Security安全框架越受大家的青睐。本文会教你用SpringSecurity设计单项目的权限,关于如何做分布式的权限,后续会跟进。

为什么选择SpringSecurity?
现如今,在JavaWeb的世界里Spring可以说是一统江湖,随着微服务的到来,SpringCloud可以说是Java程序员必须熟悉的框架,就连阿里都为SpringCloud写开源呢。(比如大名鼎鼎的Nacos)作为Spring的亲儿子,SpringSecurity很好的适应了了微服务的生态。你可以非常简便的结合Oauth做认证中心服务。本文先从最简单的单体项目开始,逐步掌握Security。更多可达官方文档
准备
我准备了一个简单的demo,具体代码会放到文末。提前声明,本demo没有用JWT,因为我想把token的维护放到服务端,更好的维护过期时间。(当然,如果将来微服务认证中心的形式,JWT也可以做到方便的维护过期时间,不做过多讨论)如果想了解Security+JWT简易入门,请戳
本项目结构如下
另外,本demo使用了MybatisPlus、lombok。
核心代码
首先需要实现两个类,一个是UserDetails的实现类SecurityUser,一个是UserDetailsService的实现类SecurityUserService。
**
* Security 要求需要实现的User类
* */
@Data
public class SecurityUser implements UserDetails {
@Autowired
private SysRoleService sysRoleService;
//用户登录名(注意此处的username和SysUser的loginName是一个值)
private String username;
//登录密码
private String password;
//用户id
private SysUser sysUser;
//该用户的所有权限
private List<SysMenu> sysMenuList;
/**构造函数*/
public SecurityUser(SysUser sysUser){
this.username = sysUser.getLoginName();
this.password = sysUser.getPassword();
this.sysUser = sysUser;
}
public SecurityUser(SysUser sysUser,List<SysMenu> sysMenuList){
this.username = sysUser.getLoginName();
this.password = sysUser.getPassword();
this.sysMenuList = sysMenuList;
this.sysUser = sysUser;
}
/**需要实现的方法*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for(SysMenu menu : sysMenuList) {
authorities.add(new SimpleGrantedAuthority(menu.getPerms()));
}
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
//默认账户未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//默认账户没有带锁
@Override
public boolean isAccountNonLocked() {
return true;
}
//默认凭证没有过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//默认账户可用
@Override
public boolean isEnabled() {
return true;
}
}
这个类包含着某个请求者的信息,在Security中叫做主体。其中这个方法是必须实现的,可以获取用户的具体权限。我们这边权限的颗粒度达到了菜单级别,而不是很多开源项目中角色那级别,我觉得颗粒度越细越方便(个人觉得...)
/**
* Security 要求需要实现的UserService类
* */
@Service
public class SecurityUserService implements UserDetailsService{
@Autowired
private SysUserService sysUserService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private HttpServletRequest httpServletRequest;
@Override
public SecurityUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
LambdaQueryWrapper<SysUser> condition = Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, loginName);
SysUser sysUser = sysUserService.getOne(condition);
if (Objects.isNull(sysUser)){
throw new UsernameNotFoundException("未找到该用户!");
}
Long projectId = null;
try{
projectId = Long.parseLong(httpServletRequest.getHeader("projectId"));
}catch (Exception e){
}
SysMenuModel sysMenuModel;
if (sysUser.getUserType()){
sysMenuModel = new SysMenuModel();
}else {
sysMenuModel = new SysMenuModel().setUserId(sysUser.getId());
}
sysMenuModel.setProjectId(projectId);
List<SysMenu> menuList = sysMenuService.getList(sysMenuModel);
return new SecurityUser(sysUser,menuList);
}
}
显而易见,这个类实现了唯一的方法loadUserByUsername,从而可以拿到某用户的所有权限,并生成主体,在后面的filter中就可以见到他的作用了。
在看配置和filter之前,还有一个类需要说明一下,此类提供方法,可以让用户未登录、或者token失效的情况下进行统一返回。
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = 1L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token失效,请登陆后重试");
}
}
ok,接下来看配置,实现了WebSecurityConfigurerAdapter的SecurityConfig类,特别说明,本demo算是前后端分离的前提下写的,所以实现过多的方法,其实这个类可以实现三个方法,具体请戳。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Autowired
SecurityFilter securityFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//禁止csrf
.csrf().disable()
//异常处理
.exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint).and()
//Session管理方式
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//开启认证
.authorizeRequests()
.antMatchers("/login/login").permitAll()
.antMatchers("/login/register").permitAll()
.antMatchers("/login/logout").permitAll()
.anyRequest().authenticated();
http
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
}
}
异常处理就是上面那个类,Session那几种管理方式我在那篇Security+JWT的文章中也有所讲解,比较简单,然后是几个不用验证的登录路径,剩下的都需要经过我们下面这个filter。
@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {
@Autowired
SecurityUserService securityUserService;
@Autowired
SysUserService sysUserService;
@Autowired
SysUserTokenService sysUserTokenService;
/**
* 认证授权
* */
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
log.info("访问的链接是:{}",httpServletRequest.getRequestURL());
try {
final String token = httpServletRequest.getHeader("token");
LambdaQueryWrapper<SysUserToken> condition = Wrappers.<SysUserToken>lambdaQuery().eq(SysUserToken::getToken, token);
SysUserToken sysUserToken = sysUserTokenService.getOne(condition);
if (Objects.nonNull(sysUserToken)){
SysUser sysUser = sysUserService.getById(sysUserToken.getUserId());
if (Objects.nonNull(sysUser)){
SecurityUser securityUser = securityUserService.loadUserByUsername(sysUser.getLoginName());
//将主体放入内存
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//放入内存中去
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}catch (Exception e){
log.error("认证授权时出错:{}", Arrays.toString(e.getStackTrace()));
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
判断用户是否登录,就是从数据库中查看是否有未过期的token,如果存在,就把主体信息放进到项目的内存中去,特别说明的是,每个请求链结束,SecurityContextHolder.getContext()的数据都会被clear的,所以,每次请求的时候都需要set。
以上就完成了Security核心的创建,为了业务代码方便获取内存中的主体信息,我特意加了一个获取用户信息的方法
/**
* 获取Security主体工具类
* @author pjjlt
* */
public class SecurityUserUtil {
public static SysUser getCurrentUser(){
SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (Objects.nonNull(securityUser) && Objects.nonNull(securityUser.getSysUser())){
return securityUser.getSysUser();
}
return null;
}
}
业务代码
以上是Security核心代码,下面简单加两个业务代码,比如登录和某个接口的权限访问测试。
万物之源登录登出
首先,不被filter拦截的那三个方法注册、登录、登出,我都写在了moudle.controller.LoginController这个路径下,注册就不用说了,就是一个insertUser的方法,做好判断就好,密码通过AES加个密。
下面看下登录代码,controller层就不说了,反正就是个验参。
/**
* 登录,返回登录信息,前端需要缓存
* */
@Override
@Transactional(rollbackFor = Exception.class)
public JSONObject login(SysUserModel sysUserModel) throws Exception{
JSONObject result = new JSONObject();
//1. 验证账号是否存在、密码是否正确、账号是否停用
Wrapper<SysUser> sysUserWrapper = Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getLoginName,sysUserModel.getLoginName()).or()
.eq(SysUser::getEmail,sysUserModel.getEmail());
SysUser sysUser = baseMapper.selectOne(sysUserWrapper);
if (Objects.isNull(sysUser)){
throw new Exception("用户不存在!");
}
String password = CipherUtil.encryptByAES(sysUserModel.getPassword());
if (!password.equals(sysUser.getPassword())){
throw new Exception("密码不正确!");
}
if (sysUser.getStatus()){
throw new Exception("账号已删除或已停用!");
}
// 2.更新最后登录时间
sysUser.setLoginIp(ServletUtil.getClientIP(request));
sysUser.setLoginDate(LocalDateTime.now());
baseMapper.updateById(sysUser);
// 3.封装token,返回信息
String token = UUID.fastUUID().toString().replace("-","");
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeSeconds);
SysUserToken sysUserToken = new SysUserToken()
.setToken(token).setUserId(sysUser.getId()).setExpireTime(expireTime);
sysUserTokenService.save(sysUserToken);
result.putOpt("token",token);
result.putOpt("expireTime",expireTime);
return result;
}
首先验证下用户是否存在,登录密码是否正确,然后封装token,值得一提的是,我并没有从数据库(sysUserToken)中获取用户已经登录的token,然后更新过期时间的形式做登录,而是每次登录都获取新token,这样就可以做到多端登录了,后期还可以做账号登录数量的控制。
然后就是登出,删除库中存在的token
/**
* 登出,删除token
* */
@Override
public void logout() throws Exception{
String token = httpServletRequest.getHeader("token");
if (Objects.isNull(token)){
throw new LoginException("token不存在",ResultEnum.LOGOUT_ERROR);
}
LambdaQueryWrapper<SysUserToken> sysUserWrapper = Wrappers.<SysUserToken>lambdaQuery()
.eq(SysUserToken::getToken,token);
baseMapper.delete(sysUserWrapper);
}
权限验证
这边我维护了两个账号,一个是超级管理员majian,拥有所有权限。一个是普通人员_pjjlt,只有一些权限,我们看一下访问接口的效果。
我们访问的接口是moudle.controller.LoginController路径下的
@PreAuthorize("hasAnyAuthority('test')")
@GetMapping("test")
public String test(){
return "test";
}
其中hasAnyAuthority('test')就是权限码
我们模拟用不同账号访问,就是改变请求header中的token值,就是登录阶段返回给前端的token。
首先是超级管理员验证
然后是普通管理员访问
接着没有登录(token不存在或者已过期)访问
demo地址
https://github.com/majian1994/easy-file-back
结束语
本文简单讲解了,主要是将Security相关的东西,具体实现角色的三要素,用户、角色、权限(菜单)可以看我的代码,都写完测完了,本来想写个文档管理系统,帮助我司更好的管理接口文档,but有位小伙伴找了一个不错的开源的了,所以这代码就成了我的一个小demo。
【Java杂货铺】用Security做权限极简入门的更多相关文章
- Spring Security极简入门三部曲(上篇)
目录 Spring Security极简入门三部曲(上篇) 写在前面 为什么要用Spring Security 数据库设计 demo时刻 核心代码讲解 小结 Spring Security极简入门三部 ...
- Spring Security极简入门三部曲(中篇)
目录 Spring Security极简入门三部曲(中篇) 验证流程 Authentication接口 过滤器链 AuthenticationProvider接口: demo时刻 代码讲解 小结 Sp ...
- request.getRemoteUser() Spring Security做权限控制后
一. request.getRemoteUser();//获取当前缓存的用户,比如Spring Security做权限控制后就会将用户登录名缓存到这里 request.getRemoteAddr(); ...
- Vue.js 入门:从零开始做一个极简 To-Do 应用
Vue.js 入门:从零开始做一个极简 To-Do 应用 写作时间:2019-12-10版本信息:Vue.js 2.6.10官网文档:https://cn.vuejs.org/ 前言 学习 Vue ...
- Git 极简入门教程学习笔记
Git 极简入门教程 http://rogerdudler.github.io/git-guide/index.zh.html 测试用 https://github.com/xxx/BrnShop. ...
- ElasticSearch极简入门总结
一,目录 安装es 项目添加maven依赖 es客户端组件注入到spring容器中 es与mysql表结构对比 索引的删除创建 文档的crud es能快速搜索的核心-倒排索引 基于倒排索引的精确搜索. ...
- .Net Core in Docker极简入门(下篇)
Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章. 目录 前言 开始 Docker-Compose 代码修改 yml file up & down 镜像仓库 最后 前言 上一篇[. ...
- .NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程
写在前面 这篇我们对用户权限进行极简设计并保留其扩展性.首先很感谢大家的阅读,前面六章我带着大家快速入门了ASP.NET Core.ASP.NET Core的启动过程源码解析及配置文件的加载过程源码解 ...
- Express + Mongoose 极简入门
今天尝试使用express + mongoose,构建了一个简单的Hello world,实现以下功能: 定义mongodb使用的Schema,一个User 访问/输出Hello world 访问/i ...
随机推荐
- 文献阅读 - MonoLoco与关于Camera Matrix的笔记
目录 概览 HighLights Camera Intrinsic Matrix 笔记 Intrinsic Matrix Task-Error - 不确定性任务下确界的计算 输出假设的Laplace分 ...
- 【分类问题中模型的性能度量(二)】超强整理,超详细解析,一文彻底搞懂ROC、AUC
文章目录 1.背景 2.ROC曲线 2.1 ROC名称溯源(选看) 2.2 ROC曲线的绘制 3.AUC(Area Under ROC Curve) 3.1 AUC来历 3.2 AUC几何意义 3.3 ...
- NET CLR via C#(第4版)第4章 类型基础
本章内容: 1 所有类型都从System.Object派生 2 类型转换 3 命名空间和程序集 4 运行时的相互关系 本章讲述使用类型和CLR时需掌握的基础知识.具体地说,要讨论所有类型都具有的一 ...
- ref与out区别
ref与out out.ref都是传递引用(内存地址),使用后都将改变原来参数的数值. ref 当调用方法时,在方法中会对ref传入的参数数值进行改变,若使用ref参数,则方法定义和调用方法都 ...
- grid布局——从入门到放弃
基本知识 CSS grid 布局有两个核心组成部分:wrapper(网格容器,父元素)和items(网格项,子元素). 基本属性 属性 含义 display: grid 网格布局(父元素设置) gri ...
- Fiddler 断点命令
Request 断点:bpu /priceCalculate 清除命令:bpu Response 断点:bpafter /priceCalculate 清除命令:bpafter
- Vue.js——5.生命周期
Vue的生命周期 创建阶段new Vue1,beforeCreate() 表示在实例没有被创建出来之前会执行它加载data和methods2,caeated() data 和methods被初始化了 ...
- Flink(四) —— 数据流编程模型
分层抽象 The lowest level abstraction simply offers stateful streaming. It is embedded into the DataStre ...
- Window Mysql5.7免安装版配置
1.下载mysql 5.7 32位:https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.19-win32.zip 5.7 64位:https ...
- Python说文解字_父类的继承
1. 第一个问题: 我们知道类是可以继承其他类的,在继承的过程中我们不光可以继承父类的方法,还可继承父类的属性,另外还可以在父类的基础上添加自己的东西. 2. 第二个问题: 我们继承父类属性和方法的时 ...