手把手教你用Java实现用户登录注册的功能

手把手教你用Java实现用户登录注册的功能

登陆注册功能

说起用户登录注册其实主要还是几个点,首先第一个就是我们常说的一些验证码。因为验证码可以防止用户频繁的请求接口,比如有一些刻意攻击的请求用来检测账户是否存在,验证码起到了至关重要的一个作用防止重复恶意请求。接着就是一个用户的一个加密密码加密,不要小看这个加密,虽然说加密的方式千变万化,但是作为微服务程序来说,大部分网站还是会用HTTPS的证书,传输还是加密传输的,只是到服务端才进行加密校验。所以总的来说只是存的数据库的密码是进行一个加密的,这里我们采用的是一个加盐的md5加密的方式,虽然说md5也之前被破解过,但是你只要多包几层应该是没有关系的,另外你还配了加盐,所以也是ok的。

登录说起用户登录,就会涉及到有一个权限问题。因为用户他分普通用户和一些管理员用户之类的。简单的一些注解判断就可以处理好了。

代码语言:javascript代码运行次数:0运行复制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失效的问题接着还说到一些用户的一些请求的token失效的问题。现在大部分都是微服务的架构,基本上都会采用redis去做一个缓存,而不是像之前的直接用web的一个session里面设置一个缓存。因为用户普遍存在于不同的微服务的服务里面,进行一个请求转发。所以放redis最好的,而且redis是天生会有一个设置过期的一个作用。

代码语言:javascript代码运行次数:0运行复制/**

* 登陆

* @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;

}注册说起注册,可能首先想到的就是验证码,验证码一般分为手机验证码或者邮箱验证码,手机验证码里可以去调用一些第三方的服务接口。然后邮箱验证码现在目前开源的一些很多第三方包也大部分包含了。所以你也无需太过担心,以下是发送短信的一些逻辑判断。

代码语言:javascript代码运行次数:0运行复制 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知道你数据的用户量,或者说推你下一个用户的一个ID。这显然是不安全的,另外的话有一些人使用uuid,我觉得这个ID好是好,就是太长了,目前来说,最常用的可能还是一些雪花算法做的一些ID。有一些也自己去配置的一些规则数字,其实雪花算法ID就有一个好处,就是方便检索,因为本身存储到数据库,它需要进行一个排序数字类的非常适合排序。

代码语言:javascript代码运行次数:0运行复制 /**

* 获取单例的Twitter的Snowflake 算法生成器对象

* 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。

*

*

* snowflake的结构如下(每部分用-分开):

*

*

* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

*

*

* 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年)

* 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)

* 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)

*

*

* 参考:http://www.cnblogs.com/relucent/p/4955340.html

*

* @return {@link Snowflake}

* @since 5.7.3

*/

IdUtil.getSnowflake().nextId()最后点赞关注评论一键三连,每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!

相关推荐