打造一款适合自己的快速开发框架-登录与权限拦截

1,953 阅读9分钟

前言

上一篇完成了权限管理,但登录与权限拦截还没做,本文简单讲述一下登录模块的设计。本快速开发框架的权限并没有采用Spring Security、Shiro等权限框架,并不是因为觉得他们做得不够好,而是觉得他们做得太好而太重了,用着不是很习惯,所以还是打算自己简单地弄一个。

关于登录

其实,登录没什么好说的,但是还是想在这简单说一下。

登录入参

常规入参都是用户名、密码、图片验证码(非必需),这里暂且使用用户名+密码的方式登录。

多次登录失败后的处理方式

当某个账号超过最大的登录错误次数时,系统自动将账号锁定,如需再次登录,需要管理员解除锁定。

关于Token存储机制

这里暂时使用的是jwt,未做持久化,后续会考虑加上db和redis的存储实现。

登录处理流程

  1. 通过用户名查询用户,存在则2,否则抛出业务异常
  2. 判断用户是否被锁定,未锁定则3,否则抛业务异常
  3. 判断用户登录错误次数是否达到阀值,未达到则4,否则抛业务异常
  4. 判断密码是否正确,正确则创建登录会话信息返回,否则抛业务异

登录相关表

用户表(sys_user)

CREATE TABLE `sys_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(32) NOT NULL COMMENT '用户名',
  `real_name` varchar(32) DEFAULT NULL COMMENT '姓名',
  `avatar` varchar(200) DEFAULT NULL COMMENT '头像',
  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  `mobile_phone` varchar(11) DEFAULT NULL COMMENT '手机号',
  `telephone` varchar(20) DEFAULT NULL COMMENT '电话',
  `password` varchar(40) DEFAULT NULL COMMENT '密码',
  `salt` varchar(10) DEFAULT NULL COMMENT '加盐',
  `sex` int(6) unsigned DEFAULT '1' COMMENT '性别(1->男|MALE,2->女|FEMALE,3->未知|UNKNOWN)',
  `is_locked` tinyint(1) unsigned DEFAULT '2' COMMENT '是否锁定(1->已锁定|YES,2->未锁定|NO)',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
  `is_deleted` tinyint(1) unsigned DEFAULT '1' COMMENT '是否删除(1->未删除|NO,2->已删除|YES)',
  PRIMARY KEY (`id`),
  KEY `real_name` (`real_name`),
  KEY `user_name` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='用户';


用户登录次数(sys_user_login_times)

CREATE TABLE `sys_user_login_times` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',
  `login_ip` char(15) DEFAULT NULL COMMENT '登录ip',
  `times` int(10) unsigned DEFAULT '1' COMMENT '登录次数',
  `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
  `is_deleted` tinyint(1) unsigned DEFAULT '1' COMMENT '是否删除(1->未删除|NO,2->已删除|YES)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户登录次数';

Spring mvc拦截器

spring mvc的拦截器是用于拦截controller方法的处理类,所有的controller类都会先经过拦截器,如果拦截器返回成功,才会执行到控制类的业务方法。自定义拦截器一般会实现HandlerInterceptor接口的方法。下面简单说一下该接口的方法

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// controller业务处理请求之前被调用,一般会在该方法下判断用户的登录状态及用户是否有权限进行下一步
        // 返回 true,则可以继续进行,返回 false,则终止。
		return true;
	}
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
        // controller业务处理请求完成之后,生成视图之前执行  
	}
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
        // 在DispatcherServlet完全处理完请求之后被调用,可用于清理资源 
	}

}

关于拦截器黑名单

拦截器黑名单即不进行拦截或拦截后直接放行的请求,一般有如下三种方式处理:

  1. 直接在配置拦截器的时候,使用excludePathPatterns排除掉不进行拦截的请求;
  2. 在拦截器里面判断是否在黑名单列表中,在就直接放行;
  3. 通过在控制层方法上加特殊的标识或注解,如果满足条件,可直接放行;

以上三种方式一般都是组合使用的,根据不同的场景,使用不同的处理方式。

拦截器处理流程

  1. 读取控制层方法上的注解,判断是否存在放行注解,不存在则2,否则放行
  2. 判断token是否存在,存在则3,否则抛业务异常
  3. 判断token是否合法,合法则4,否则抛业务异常
  4. 判断该用户是否有权限,有则放行,无则抛业务异常

开始编码

目录结构

├── mldong-admin  管理端接口
	├── src/main/java
		├──	com.mldong.modules.sys
			├── controller
				└──	SysLoginController.java
			├──	dto
				└──	SysLoginParam.java
			├──	service
				├── impl
                    ├── SysLoginServiceImpl.java
                    └──	SysRbacServiceImpl.java
                └── SysLoginService.java
            └──	vo
            	└──	SysLoginVo.java
	├── src/main/resources
		└── dao/sys
			└── sys_user_dao.xml
├── mldong-common  工具类及通用代码
	├── src/main/java
		├──	com.mldong.common
			├──	annotation
				└──	AuthIgnore.java
			├── config
				└── InterceptorConfig.java
			├── interceptor
				├── AuthInterceptor.java
				└── AuthInterceptorService.java
			├── jwt
				├──	JwtProperties.java
				└──	JwtToken.java
			└── token
				├──	impl
					└──	JwtTokenStrategyImpl.java
				└──	TokenStrategy.java
├── mldong-generator  代码生成器

核心文件说明

  • mldong-common/src/main/java/com/mldong/common/token/TokenStrategy.java

token存储策略接口

package com.mldong.common.token;

public interface TokenStrategy {
	/**
	 * 通过用户id和用户生成token
	 * @param userId 用户id
	 * @param userName 用户密码
	 * @return
	 */
	public String generateToken(Long userId,String userName);
	/**
	 * 校验token是否合法
	 * @param token 临时令牌
	 * @return
	 */
	public boolean verifyToken(String token);
	/**
	 * 从token中获取用户id
	 * @param token
	 * @return
	 */
	public Long getUserId(String token);
	/**
	 * 从token中获取用户名
	 * @param token
	 * @return
	 */
	public String getUserName(String token);
}
  • mldong-common/src/main/java/com/mldong/common/token/impl/JwtTokenStrategyImpl.java

token存储,jwt实现类

package com.mldong.common.token.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mldong.common.jwt.JwtToken;
import com.mldong.common.token.TokenStrategy;

@Component
public class JwtTokenStrategyImpl implements TokenStrategy{
	@Autowired
	private JwtToken jwtToken;

	@Override
	public String generateToken(Long userId, String userName) {
		return jwtToken.generateToken(userId, userName);
	}
	@Override
	public boolean verifyToken(String token) {
		return jwtToken.verify(token);
	}
	@Override
	public Long getUserId(String token) {
		return jwtToken.getUserId(token);
	}
	@Override
	public String getUserName(String token) {
		return jwtToken.getUserName(token);
	}
}
  • mldong-common/src/main/java/com/mldong/common/jwt/JwtToken.java

jwt bean略

  • mldong-common/src/main/java/com/mldong/annotation/AuthIgnore.java

控制层方法放行注解

package com.mldong.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 忽略权限的注解
 * @author mldong
 *
 */
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthIgnore {

}
  • mldong-common/src/main/java/com/mldong/interceptor/AuthInterceptor.java
package com.mldong.common.interceptor;

import io.swagger.annotations.ApiOperation;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import com.mldong.common.access.AccessInitProcessor;
import com.mldong.common.annotation.AuthIgnore;
import com.mldong.common.base.constant.CommonConstants;
import com.mldong.common.base.constant.GlobalErrEnum;
import com.mldong.common.exception.BizException;
@Component
public class AuthInterceptor implements HandlerInterceptor {
	@Autowired(required=false)
	private AuthInterceptorService authInterceptorService;
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {
		if(handler.getClass().isAssignableFrom(HandlerMethod.class)) {
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			AuthIgnore authIgnore = handlerMethod.getMethodAnnotation(AuthIgnore.class);
			if(null != authIgnore) {
				// 要忽略权限
				return true;
			}
			String token = getToken(request);
			if("".equals(token)) {
				throw new BizException(GlobalErrEnum.GL99990401);
			}
			if(authInterceptorService == null) {
				// 空就不做处理了
				return true;
			}
			if(!authInterceptorService.verifyToken(token)) {
				// token校验不通过
				throw new BizException(GlobalErrEnum.GL99990401);
			}
			ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
			String access = AccessInitProcessor.getAccess(apiOperation);
			if(null == access) {
				// 没有定义,直接放行
				return true;
			}
			if(!authInterceptorService.hasAuth(token, access)){
				// 无权限访问
				throw new BizException(GlobalErrEnum.GL99990403);
			}
		}
		return true;
	}
	private String getToken(HttpServletRequest request) {
		String token = "";
		token = request.getHeader(CommonConstants.TOKEN);
		if(StringUtils.isEmpty(token)) {
			token = request.getParameter(CommonConstants.TOKEN);
		}
		return token;
	}
}
  • mldong-common/src/main/java/com/mldong/interceptor/AuthInterceptorService.java

提供给拦截器的业务接口,由业务模块实现,这里放在了SysRbacServiceImpl.java类上实现了

package com.mldong.common.interceptor;
/**
 * 权限拦截器所需要的服务,由业务层实现
 * @author mldong
 *
 */
public interface AuthInterceptorService {
	/**
	 * 校验token
	 * @param token
	 * @return
	 */
	public boolean verifyToken(String token);
	/**
	 * 用户是否有权限
	 * @param token token
	 * @param access 权限标识
	 * @return
	 */
	public boolean hasAuth(String token,String access);
}
  • mldong-admin/src/main/java/com/mldong/modules/sys/SysRbacServiceImpl.java

拦截器类实现,代码片段


@Service
public class SysRbacServiceImpl implements SysRbacService, AuthInterceptorService{
	@Autowired
	private AccessInitProcessor accessInitProcessor;
	@Autowired
	private SysUserDao sysUserDao;
	@Autowired
	private SysUserRoleMapper sysUserRoleMapper;
	@Autowired
	private SysRoleAccessMapper sysRoleAccessMapper;
	@Autowired
	private SysRoleMenuMapper sysRoleMenuMapper;
	@Autowired
	private GlobalProperties globalProperties;
	@Autowired
	private TokenStrategy tokenStrategy;
    @Override
	public boolean hasAccess(Long userId, String access) {
		if(userId.equals(globalProperties.getSuperAdminId())) {
			return true;
		}
 		return loadUserAccessList(userId).contains(access);
	}
	@Override
	public boolean verifyToken(String token) {
		return tokenStrategy.verifyToken(token);
	}
	@Override
	public boolean hasAuth(String token, String access) {
		Long userId = tokenStrategy.getUserId(token);
		return hasAccess(userId, access);
	}
}
  • mldong-common/src/main/java/com/mldong/config/InterceptorConfig.java

自定义拦截器配置,排除了swaggerui

package com.mldong.common.config;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
	@Autowired(required=false)
	private List<HandlerInterceptor> interceptorList;
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		if(interceptorList!=null) {
			interceptorList.forEach(interceptor->{
				registry.addInterceptor(interceptor)
				.excludePathPatterns("/swagger-resources/**");
			});
		}
	}
}
  • mldong-admin/src/main/java/com/mldong/modules/sys/SysLoginService.java

登录接口定义

package com.mldong.modules.sys.service;

import com.mldong.modules.sys.dto.SysLoginParam;
import com.mldong.modules.sys.vo.SysLoginVo;

/**
 * 登录接口
 * @author mldong
 *
 */
public interface SysLoginService {
	/**
	 * 登录
	 * @param param
	 * @return
	 */
	public SysLoginVo login(SysLoginParam param);
	/**
	 * 退出
	 * @param token 临时凭证
	 * @return
	 */
	public int logout(String token);
}
  • mldong-admin/src/main/java/com/mldong/modules/sys/impl/SysLoginServiceImpl.java

登录实现类

package com.mldong.modules.sys.service.impl;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.mldong.common.base.YesNoEnum;
import com.mldong.common.config.GlobalProperties;
import com.mldong.common.exception.BizException;
import com.mldong.common.token.TokenStrategy;
import com.mldong.common.tool.Md5Tool;
import com.mldong.modules.sys.dto.SysLoginParam;
import com.mldong.modules.sys.entity.SysUser;
import com.mldong.modules.sys.entity.SysUserLoginTimes;
import com.mldong.modules.sys.enums.SysErrEnum;
import com.mldong.modules.sys.mapper.SysUserLoginTimesMapper;
import com.mldong.modules.sys.mapper.SysUserMapper;
import com.mldong.modules.sys.service.SysLoginService;
import com.mldong.modules.sys.service.SysRbacService;
import com.mldong.modules.sys.vo.SysLoginVo;
/**
 * 登录接口实现
 * @author mldong
 *
 */
@Service
public class SysLoginServiceImpl implements SysLoginService{
	@Autowired
	private SysUserMapper sysUserMapper;
	@Autowired
	private SysUserLoginTimesMapper sysUserLoginTimesMapper;
	@Autowired
	private GlobalProperties globalProperties;
	@Autowired
	private TokenStrategy generateTokenStrategy;
	@Autowired
	private SysRbacService sysRbacService;
	@Transactional(rollbackFor=Exception.class)
	@Override
	public SysLoginVo login(SysLoginParam param) {
		String userName = param.getUserName();
		String password = param.getPassword();
		SysUser q = new SysUser();
		q.setUserName(userName);
		SysUser user = sysUserMapper.selectOne(q);
		if(null == user) {
			// 用户不存在
			throw new BizException(SysErrEnum.SYS80000001);
		}
		if(YesNoEnum.YES.equals(user.getIsLocked())) {
			//用户已被锁定
			throw new BizException(SysErrEnum.SYS80000004);
		}
		// 校验登录次数
		SysUserLoginTimes userLoginTimesQuery = new SysUserLoginTimes();
		userLoginTimesQuery.setUserId(user.getId());
		SysUserLoginTimes userLoginTimes = sysUserLoginTimesMapper.selectOne(userLoginTimesQuery);
		if(null == userLoginTimes || 
			(null != userLoginTimes && userLoginTimes.getTimes()<=globalProperties.getMaxErrLoginTimes())) {
			String passwordEncry = Md5Tool.md5(password, user.getSalt());
			if(!passwordEncry.equals(user.getPassword())) {
				// 用户名或者密码错误
				Date now = new Date();
				if(userLoginTimes == null) {
					userLoginTimes = new SysUserLoginTimes();
					userLoginTimes.setUserId(user.getId());
					userLoginTimes.setCreateTime(now);
					userLoginTimes.setUpdateTime(now);
					userLoginTimes.setTimes(1);
					sysUserLoginTimesMapper.insertSelective(userLoginTimes);
				} else {
					userLoginTimes.setUserId(null);
					userLoginTimes.setCreateTime(null);
					userLoginTimes.setUpdateTime(now);
					userLoginTimes.setTimes(userLoginTimes.getTimes()+1);
					sysUserLoginTimesMapper.updateByPrimaryKeySelective(userLoginTimes);
				}
				throw new BizException(SysErrEnum.SYS80000002);
			}
		} else {
			// 登录次数太多,账号已被锁定,请联系管理员
			throw new BizException(SysErrEnum.SYS80000003);
		}
		return createLoginVo(user);
	}
	@Transactional(rollbackFor=Exception.class)
	@Override
	public int logout(String token) {
		return 1;
	}
	/**
	 * 创建登录返回vo
	 * @param user
	 * @return
	 */
	private SysLoginVo createLoginVo (SysUser user) {
		SysLoginVo vo = new SysLoginVo();
		Long userId = user.getId();
		String avatar = user.getAvatar();
		String userName = user.getUserName();
		String realName = user.getRealName();
		// 创建token
		String token = generateTokenStrategy.generateToken(userId, userName);
		vo.setAccessList(sysRbacService.loadUserAccessList(userId));
		vo.setAvatar(avatar);
		vo.setMenuList(sysRbacService.loadUserMenuList(userId));
		vo.setRealName(realName);
		vo.setUserId(userId);
		vo.setUserName(userName);
		vo.setToken(token);
		return vo;
	}
}
  • mldong-admin/src/main/java/com/mldong/modules/sys/controller/SysLoginController.java

登录控制层类

package com.mldong.modules.sys.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.mldong.common.annotation.AuthIgnore;
import com.mldong.common.base.CommonResult;
import com.mldong.common.base.constant.CommonConstants;
import com.mldong.modules.sys.dto.SysLoginParam;
import com.mldong.modules.sys.service.SysLoginService;
import com.mldong.modules.sys.vo.SysLoginVo;

@RestController
@Api(tags="sys-登录模块")
public class SysLoginController {
	@Autowired
	private SysLoginService sysLoginService;
	/**
	 * 登录系统
	 * @param param
	 * @return
	 */
	@PostMapping("/sys/login")
	@AuthIgnore
	@ApiOperation(value="登录系统", notes="登录系统")
	public CommonResult<SysLoginVo> login(@RequestBody @Validated SysLoginParam param) {
		return CommonResult.success("登录成功", sysLoginService.login(param));
	}
	/**
	 * 退出系统
	 * @return
	 */
	@PostMapping("/sys/logout")
	@ApiOperation(value="退出系统", notes="退出系统")
	@AuthIgnore
	public CommonResult<?> logout(HttpServletRequest request) {
		String token = getToken(request);
		sysLoginService.logout(token);
		return CommonResult.success("退出成功", null);
	}
	private String getToken(HttpServletRequest request) {
		String token = "";
		token = request.getHeader(CommonConstants.TOKEN);
		if(StringUtils.isEmpty(token)) {
			token = request.getParameter(CommonConstants.TOKEN);
		}
		return token;
	}
}

  • mldong-admin/src/main/java/com/mldong/modules/sys/vo/SysLoginVo.java

登录成功返回vo

package com.mldong.modules.sys.vo;

import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;
import java.util.List;

import com.mldong.modules.sys.entity.SysMenu;

public class SysLoginVo implements Serializable{

	/**
	 * 
	 */
	private static final long serialVersionUID = 1387112822760941352L;
	
	@ApiModelProperty(value="临时令牌")
	private String token;
	@ApiModelProperty(value="用户id")
	private Long userId;
	@ApiModelProperty(value="用户名")
	private String userName;
	@ApiModelProperty(value="姓名")
	private String realName;
	@ApiModelProperty(value="头像")
	private String avatar;
	@ApiModelProperty(value="权限标识集合")
	private List<String> accessList;
	@ApiModelProperty(value="路由菜单集合")
	private List<SysMenu> menuList;

	// get set 略
}

小结

登录看似简单的功能,但是要真正做好,也并不是很容易。因为不只是要考虑能实现,要还考虑后续的扩展,就比如token的存储机制、拦截器业务方法抽离等面向接口编程。。。。。。

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-后端脚手架搭建

打造一款适合自己的快速开发框架-集成mapper

打造一款适合自己的快速开发框架-集成swaggerui和knife4j

打造一款适合自己的快速开发框架-通用类封装之统一结果返回、统一异常处理

打造一款适合自己的快速开发框架-业务错误码规范及实践

打造一款适合自己的快速开发框架-框架分层及CURD样例

打造一款适合自己的快速开发框架-mapper逻辑删除及枚举类型规范

打造一款适合自己的快速开发框架-数据校验之Hibernate Validator

打造一款适合自己的快速开发框架-代码生成器原理及实现

打造一款适合自己的快速开发框架-通用查询设计与实现

打造一款适合自己的快速开发框架-基于rbac的权限管理