Spring Security单项目权限设计过程解析

网友投稿 296 2022-12-22


Spring Security单项目权限设计过程解析

为什么选择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 sysMenuList;

/**构造函数*/

public SecurityUser(SysUser sysUser){

this.username = sysUser.getLoginName();

this.password = sysUser.getPassword();

this.sysUser = sysUser;

}

public SecurityUser(SysUser sysUser,List sysMenuList){

this.username = sysUser.getLoginName();

this.password = sysUser.getPassword();

this.sysMenuList = sysMenuList;

this.sysUser = sysUser;

}

/**需要实现的方法*/

@Override

public Collection extends GrantedAuthority> getAuthorities() {

List 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 condition = Wrappers.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 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 condition = Wrappers.lambdaQuery().eq(SysUserToken::getToken, token);

SysUserToken sysUserToken = sysUserTokenService.getOnepQZNhrscL(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

* */

publichttp:// 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 sysUserWrapper = Wraphttp://pers.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 sysUserWrapper = Wrappers.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。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:MyBatis Plus构建一个简单的项目的实现
下一篇:spring如何加载配置多个配置文件
相关文章

 发表评论

暂时没有评论,来抢沙发吧~