说起用户登录注册其实主要还是几个点,首先第一个就是我们常说的一些验证码。因为验证码可以防止用户频繁的请求接口,比如有一些刻意攻击的请求用来检测账户是否存在,验证码起到了至关重要的一个作用防止重复恶意请求。接着就是一个用户的一个加密密码加密,不要小看这个加密,虽然说加密的方式千变万化,但是作为微服务程序来说,大部分网站还是会用HTTPS的证书,传输还是加密传输的,只是到服务端才进行加密校验。所以总的来说只是存的数据库的密码是进行一个加密的,这里我们采用的是一个加盐的md5加密的方式,虽然说md5也之前被破解过,但是你只要多包几层应该是没有关系的,另外你还配了加盐,所以也是ok的。
说起用户登录,就会涉及到有一个权限问题。因为用户他分普通用户和一些管理员用户之类的。简单的一些注解判断就可以处理好了。
public class PermissionInterceptor implements HandlerInterceptor {
private static final Logger log = LogManager.getLogger();
@Autowired
private TokenCacheService tokenCache;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 静态目录
*/
if (handler instanceof ResourceHttpRequestHandler) {
return true;
} else if (!(handler instanceof HandlerMethod)) {
log.error("This handler object is not HandlerMethod instance! handler class: {}", handler.getClass());
return true;
}
log.debug("preHandle:{}", request.getRequestURI());
try {
Method method = ((HandlerMethod) handler).getMethod();
Annotation[] annotations = method.getAnnotations();
/**
* 不需要登录的放行
*/
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(NotNeedLogin.class)) {
return true;
}
}
String token = request.getHeader(AuthConstants.TOKEN);
/**
* 空的tokne
*/
if (StrUtil.isBlankIfStr(token)) {
responseError(response, ResultCode.UNAUTHORIZED);
return false;
}
/**
* token过期
*/
CacheToken tokenCache = this.tokenCache.getTokenCache(token);
if (tokenCache == null) {
responseError(response, ResultCode.UNAUTHORIZED);
return false;
}
/**
* 鉴权
*/
/**
* 超级管理员直接放行
*/
// if (SimUserRoleEnum.ADMIN.getCode() == tokenCache.getRoleLevel()) {
// this.tokenCache.refreshToken(token);
// return true;
// }
/**
* 普通用户鉴权
*/
for (Annotation annotation : annotations) {
/**
* 权限标识和超级管理员标识只会二选一,不存在
*/
if (annotation.annotationType().equals(RequiresAuthority.class)) {
RequiresAuthority authority = (RequiresAuthority) annotation;
/**
* 管理员用户只能查看管理员的权限
*/
if(tokenCache.hasAdmin() && !containsAdminPermissions(authority.value())){
log.info("用户{}无权限访问的{}方法{}权限需要:{}", method.getDeclaringClass().getName(),
method.getName(), authority.value());
permissionError(response, ResultCode.FORBIDDEN);
return false;
/**
* 判断用户是否有权限
*/
} else if (!containsPermissions(authority.value(), tokenCache.getAuthority())) {
log.info("用户{}无权限访问的{}方法{}权限需要:{}", method.getDeclaringClass().getName(),
method.getName(), authority.value());
permissionError(response, ResultCode.FORBIDDEN);
return false;
}
} else if (!tokenCache.hasAdmin() && annotation.annotationType().equals(Administrator.class)) {
/**
* 不是管理员访问超级管理员接口直接禁止
*/
permissionError(response, ResultCode.FORBIDDEN);
return false;
}
}
this.tokenCache.refreshToken(token);
return true;
} catch (Exception e) {
log.error("拦截异常:", e);
if (e instanceof ApiException) {
ApiException apiException = (ApiException) e;
Response.filterError(response, apiException.getCode(), apiException.getMsg());
} else {
responseError(response, ResultCode.FAILED);
}
return false;
}
}
private boolean containsAdminPermissions(AuthorityEnum[] value) {
for (AuthorityEnum auth : value) {
/**
* 管理员权限只可以查看包含管理的权限的方法
* note:可能后期权限更改
*/
if (auth.getCode() == AuthorityEnum.MANAGEMENT.getCode()) {
return true;
}
}
return false;
}
private void permissionError(HttpServletResponse response, IErrorCode unauthorized) {
Response.permissionError(response, unauthorized.getCode(),
unauthorized.getMessage());
}
private void responseError(HttpServletResponse response, IErrorCode unauthorized) {
Response.filterError(response, unauthorized.getCode(),
unauthorized.getMessage());
}
/**
* 判断是否有权限
* @param value 权限枚举数组(方法权限标识)
* @param authority 用户权限等级
* @return
*/
private boolean containsPermissions(AuthorityEnum[] value, Integer authority ) {
for (AuthorityEnum auth : value) {
/**
* 普通权限可以查看普通加只读
*/
if (authority.equals(AuthorityEnum.NORMAL.getCode())) {
if(auth.getCode() == AuthorityEnum.NORMAL.getCode()
|| auth.getCode() == AuthorityEnum.READ_ONLY.getCode()){
return true;
}
}
/**
* 只读权限只能只读
*/
if (authority.equals(AuthorityEnum.READ_ONLY.getCode())
&& auth.getCode() == AuthorityEnum.READ_ONLY.getCode()) {
return true;
}
}
return false;
}
}
接着还说到一些用户的一些请求的token失效的问题。现在大部分都是微服务的架构,基本上都会采用redis去做一个缓存,而不是像之前的直接用web的一个session里面设置一个缓存。因为用户普遍存在于不同的微服务的服务里面,进行一个请求转发。所以放redis最好的,而且redis是天生会有一个设置过期的一个作用。
/**
* 登陆
* @param loginDto
* @return
*/
public TokenVo login(LoginDto loginDto) {
SimUser user = findUserByUsername(loginDto.getPhone());
if (user == null) {
Asserts.fail(ResultCode.USER_OR_PASSWORD_ERROR);
}
/**
* 检查用户权限
*/
if (AuthorityEnum.DISABLED.getCode() == user.getAuthority()) {
Asserts.fail(ResultCode.USER_DISABLED);
}
/**
* 检查是否不允许登录
*/
if (redisUtil.hasKey(RedisCacheKey.USER_LOGIN_LOCK + user.getId())){
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
long expSec = redisUtil.ttl(RedisCacheKey.USER_LOGIN_LOCK + user.getId());
long timestamp = System.currentTimeMillis() + expSec * 1000;
String timeStr = sdf.format(new Date(Long.valueOf(timestamp).longValue()));
throw new ApiException("连续5次输入密码错误,请于 " + timeStr + " 后再尝试登录");
}
/**
* 优先使用密码登录
* 1.默认密码登录强制修改密码
* 2.验证密码正确性
*
*/
if (StrUtil.isNotBlank(loginDto.getPwd())) {
/**
* 验证密码格式
*/
if (!PwdUtils.validatePwdFormat(loginDto.getPwd())) {
Asserts.fail(ResultCode.USER_OR_PASSWORD_ERROR);
}
/**
* 验证密码,默认流程
*/
if (checkPassword(user, loginDto.getPwd())) {
if (ComConstant.DEFAULT_PWD.equals(loginDto.getPwd())) {
/**
* 如果时初始密码并且能登录成功,代表第一次登录,需要强制修改密码,不存入缓存
*/
return TokenVo.firstLogin(user);
}
redisUtil.del(RedisCacheKey.USER_LOGIN_PWD_RETRY + user.getId());
return setUserLoginTokenCache(user, loginDto.isFifteenFreeLogin());
} else {
//一日内,用户连续输入5次密码错误,则锁定半小时不允许账号密码登录
Integer num = (Integer) redisUtil.get(RedisCacheKey.USER_LOGIN_PWD_RETRY + user.getId());
num = (null == num) ? 1 : ++num;
if (num >= 5){
redisUtil.set(RedisCacheKey.USER_LOGIN_LOCK + user.getId(), user,30*60);
} else {
redisUtil.set(RedisCacheKey.USER_LOGIN_PWD_RETRY + user.getId(), num, DateUtils.getRemainSecondsOneDay());
}
Asserts.fail(ResultCode.USER_OR_PASSWORD_ERROR);
}
/**
* 或者使用验证码登录
*/
}else if (StrUtil.isNotBlank(loginDto.getCode())) {
if (smsService.verifyCaptcha(loginDto.getPhone(), loginDto.getCode())) {
/**
* 如果时初始密码并且能登录成功,代表第一次登录,需要强制修改密码,不存入缓存
*/
if (checkPassword(user, ComConstant.DEFAULT_PWD)) {
/**
* 如果时初始密码并且能登录成功,代表第一次登录,需要强制修改密码,不存入缓存
*/
return TokenVo.firstLogin(user);
}
return setUserLoginTokenCache(user, loginDto.isFifteenFreeLogin());
} else {
Asserts.fail(ResultCode.CAPTCHA_ERROR);
}
}
Asserts.fail(ResultCode.USER_OR_PASSWORD_ERROR);
return null;
}
说起注册,可能首先想到的就是验证码,验证码一般分为手机验证码或者邮箱验证码,手机验证码里可以去调用一些第三方的服务接口。然后邮箱验证码现在目前开源的一些很多第三方包也大部分包含了。所以你也无需太过担心,以下是发送短信的一些逻辑判断。
public boolean sendSms(SmsPhoneDto smsPhoneDto) {
//短信开关
if (!SendSmsConfig.SMS_SWITCH_OPEN.equals(smsConfig.getSmsSwitch())) {
return true;
}
//校验手机号是否合法
if (!Validator.isMobile(smsPhoneDto.getPhone())) {
Asserts.fail(ResultCode.PHONE_NUMBER_IS_ILLEGAL);
}
//使用短信验证码登录须为系统非不可用用户
if (null != smsPhoneDto.getIsLogin() && smsPhoneDto.getIsLogin()) {
SimUser user = simUserDao.findUserByPhone(smsPhoneDto.getPhone());
if (user == null) {
Asserts.fail(ResultCode.USER_NOT_EXIST);
}
if (AuthorityEnum.DISABLED.getCode() == user.getAuthority()) {
Asserts.fail(ResultCode.USER_DISABLED);
}
}
//60秒内重复发送校验
if (redisUtil.hasKey(RedisCacheKey.USER_CAPTCHA_INTERVAL_PREFIX + smsPhoneDto.getPhone())) {
Asserts.fail("已经发送验证码,请勿重复发送!");
}
//同用户,每天20次上限校验
Integer count = (Integer) redisUtil.get(RedisCacheKey.USER_VERIFY_CODE_SEND_COUNT_PREFIX + smsPhoneDto.getPhone());
count = (null == count) ? 1 : ++count;
if (count > Integer.valueOf(smsConfig.getVerifyCodeSendLimit())){
Asserts.fail(ResultCode.VERIFY_CODE_LIMIT_ERROR);
}
redisUtil.set(RedisCacheKey.USER_VERIFY_CODE_SEND_COUNT_PREFIX + smsPhoneDto.getPhone(), count, DateUtils.getRemainSecondsOneDay());
int code = RandomUtil.getRandom().nextInt(111111, 1000000);
Boolean sendStatus = smsUtil.sendSmsSingle(smsConfig.getVerifyCodeTemplateId(),
new String[]{code + "", smsConfig.getVerifyCodeEffectiveTime()}, smsPhoneDto.getPhone());
if (!sendStatus) {
return false;
}
redisUtil.set(RedisCacheKey.USER_CAPTCHA_INTERVAL_PREFIX + smsPhoneDto.getPhone(), code,
cacheConfig.getCaptchaSendInterval() * 60);
redisUtil.set(RedisCacheKey.USER_CAPTCHA_PREFIX + smsPhoneDto.getPhone(), String.valueOf(code), cacheConfig.getCaptchaDuration() * 60);
return true;
}
最后也是这个更重要的一个点就是关于用户的一个ID。因为你用户的ID时常可能需要保存到缓存或者到页面上面做一些呈现,你自增的ID肯定是不行的。因为用户可以根据你的ID知道你数据的用户量,或者说推你下一个用户的一个ID。这显然是不安全的,另外的话有一些人使用uuid,我觉得这个ID好是好,就是太长了,目前来说,最常用的可能还是一些雪花算法做的一些ID。有一些也自己去配置的一些规则数字,其实雪花算法ID就有一个好处,就是方便检索,因为本身存储到数据库,它需要进行一个排序数字类的非常适合排序。
/**
* 获取单例的Twitter的Snowflake 算法生成器对象<br>
* 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。
*
* <p>
* snowflake的结构如下(每部分用-分开):<br>
*
* <pre>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* </pre>
* <p>
* 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>
* 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)<br>
* 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
*
* <p>
* 参考:http://www.cnblogs.com/relucent/p/4955340.html
*
* @return {@link Snowflake}
* @since 5.7.3
*/
IdUtil.getSnowflake().nextId()
点赞关注评论一键三连,每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!