java实现用户登录_SpringBoot 实现用户登录,分布式Session功能

0f6fbf4f66d82e2f77809cd7110ab453.gif
7eb2127b09c98c891a140dbe95d43b23.png

之前介绍过不少关于登录功能的代码,本文介绍一下关于分布式Session 的功能实现,

完整代码(以后写博客,尽量给 git 地址)在 https://github.com/saysky/sensboot

通常,我们的项目都是多实例部署的,生产环境通常一个模块可能都会部署四五台服务器。

我们知道用户登录后,需要存储 session 信息,session 信息通常是存储在服务器的内存中的,不能持久化(服务器重启失效),多台服务器也不能共存。为了解决这个问题,我们可以将 session 存到几个服务器共享的地方里去,比如 Redis,只要在一个内网中,几台服务器可以共享 Redis (Redis本质也是装在某台服务器中)。

具体怎么实现呢?这里简单描述下:

  1. 用户登录成功,通过UUID生成一个随机唯一字符串,命名为 token,通过向 redis 中 set 一个值,key 为 token 字符串,value 为用户对象序列化后的字符串。

  2. 当用户访问其他页面,请求方法时,检验请求参数或 cookie 中是否有 token

  3. 如果有,则从 redis 查询 token,验证 token 是否有效

  4. 如果没有,则抛出异常 “用户未登录”

关于参数验证,这里可以通过 SpringMVC 的 resolveArgument 方法来统一解决,即所有方法参数验证时都会验证用户名是否登录。而不需要在每个方法里都写一段检查用户名是否登录,这样就太冗余了。

下面是具体实现,由上到下(重要到次要)贴代码,完整代码在 GitHub 中可以获取。

一、基本登录

LoginController

登录的实现在 UserServiceImpl 中

package com.liuyanzhao.sens.controller;
import com.liuyanzhao.sens.result.Result;
import com.liuyanzhao.sens.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
/**
* @author 言曌
* @date 2019-07-22 14:07
*/
@Controller
@Slf4j
public class LoginController {
@Autowired
private UserService userService;
/**
* 登录功能
* 验证用户名和密码,登录成功,生成token,存入到redis中
* 登录成功
*
* @param response
* @param username
* @param password
* @return
*/
@PostMapping("/doLogin")
@ResponseBody
public Result doLogin(HttpServletResponse response,
@RequestParam("username") String username,
@RequestParam("password") String password) {
//登录
log.info("用户登录:username:{}, password:{}", username, password);
//判断用户名是否存在
String token = userService.login(response, username, password);
return Result.success(token);
}
}

UserServiceImpl

为了代码简洁,UserService 接口这里就不贴了

package com.liuyanzhao.sens.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.plugins.Page;
import com.liuyanzhao.sens.entity.User;
import com.liuyanzhao.sens.exception.GlobalException;
import com.liuyanzhao.sens.mapper.UserMapper;
import com.liuyanzhao.sens.result.CodeMsg;
import com.liuyanzhao.sens.service.UserService;
import com.liuyanzhao.sens.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* 用户业务逻辑实现类
* MyBatis Plus 版本
*/
@Service
public class UserServiceImpl implements UserService {
public static final String COOKIE_NAME_TOKEN = "token";
/**
* token过期时间,2天
*/
public static final int TOKEN_EXPIRE = 3600 * 24 * 2;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
@Override
public String login(HttpServletResponse response, String username, String password) {
//判断用户名是否存在
User user = findByUsername(username);
if (user == null) {
throw new GlobalException(CodeMsg.USERNAME_NOT_EXIST);
}
//验证密码,这里为了例子简单,密码没有加密
String dbPass = user.getPassword();
if (!password.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUID.randomUUID().toString().replace("-", "");
addCookie(response, token, user);
return token;
}
@Override
public User getByToken(HttpServletResponse response, String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
User user = JSON.parseObject(redisUtil.get(COOKIE_NAME_TOKEN + "::" + token), User.class);
//重置有效期
if (user == null) {
throw new GlobalException(CodeMsg.USER_NOT_LOGIN);
}
addCookie(response, token, user);
return user;
}
private void addCookie(HttpServletResponse response, String token, User user) {
//将token存入到redis
redisUtil.set(COOKIE_NAME_TOKEN + "::" + token, JSON.toJSONString(user), TOKEN_EXPIRE);
//将token写入cookie
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
cookie.setMaxAge(TOKEN_EXPIRE);
cookie.setPath("/");
response.addCookie(cookie);
}
}

UserMapper Dao层 和 User 实体 这里也不贴了

我相信你都学到了分布式 Session 这里,MyBatis 的基本使用应该不成问题吧

GitHub里也有完整代码

登录成功,目前密码是没有加密的,登录成功,data里有 token 字符串,前端可以将 token

存起来,比如 APP 端,没有 cookie 这种东西的话,可以存在 localStorage,然后请求的时候携带 token 到参数上即可。

目前我们后端是将 token 存在 cookie 里,所以前端非APP端,无需携带参数。

a664460f544d8448a43771d1f6571f49.png

二、封装参数验证

UserArgumentResolver

package com.liuyanzhao.sens.config;
import com.liuyanzhao.sens.entity.User;
import com.liuyanzhao.sens.exception.GlobalException;
import com.liuyanzhao.sens.result.CodeMsg;
import com.liuyanzhao.sens.service.UserService;
import com.liuyanzhao.sens.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 用户参数验证,验证是否有token
*/
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class> clazz = parameter.getParameterType();
return clazz == User.class;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
String paramToken = request.getParameter(UserServiceImpl.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, UserServiceImpl.COOKIE_NAME_TOKEN);
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
// return null;
throw new GlobalException(CodeMsg.USER_NOT_LOGIN);
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length <= 0) {
// return null;
throw new GlobalException(CodeMsg.TOKEN_INVALID);
}
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}

WebConfig

package com.liuyanzhao.sens.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @author 言曌
* @date 2019-06-26 22:37
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List argumentResolvers) {
//所有方法执行前,都会验证参数,检查token是否存在
argumentResolvers.add(userArgumentResolver);
}
/**
* 解决跨域
* @return
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
}

如上配置,可以实现 SpringMVC 验证参数时,会执行上面我们重写的 resolveArgument  方法,该方法的目的是验证请求参数或 cookie 中是否有 token,如果有则根据 token 查询用户,然后返回(如果返回了 user 对象,会自动注入到 参数中),如下 UserController 中 current 方法示例,User user 里已有用户信息。

验证请求

UserController

package com.liuyanzhao.sens.controller;
import com.liuyanzhao.sens.entity.User;
import com.liuyanzhao.sens.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 后台用户管理控制器
* 异常不用捕获,用统一异常捕获处理
*
* @author liuyanzhao
*/
@Slf4j
@RestController
@RequestMapping(value = "/user")
public class UserController {
/**
* 当前登录用户
*
* @param user 用户信息,由封装注入
* @return
*/
@RequestMapping("/current")
@ResponseBody
public Result current(User user) {
return Result.success(user);
}
}

这是登录后的,调用查询查询当前登录用户信息接口,即对应上面的方法

注意:cookies 不用自己客户端(请求方)管理,如果 浏览器或者 PostMan 对应该域名有 cookie 的话,会自动携带的,后端能直接获取的。

b0eb094fa25a338a24c05ad66f94c387.png

如果我这里,浏览器(或PostMan)清除 cookie,或者 token 过期,再次请求,就会返回用户未登录的状态信息

a4c4c5832bd7b956e6170752912c3ffe.png

三、返回体和响应码封装

封装返回对象

package com.liuyanzhao.sens.result;
public class Result {
private int code;
private String msg;
private T data;
/**
* 成功时候的调用
*/
public static Result success() {
return new Result(200, "成功");
}
/**
* 成功时候的调用
*/
public static Result success(T data) {
return new Result(200, "成功", data);
}
/**
* 失败时候的调用
*/
public static Result error(CodeMsg codeMsg) {
return new Result(codeMsg);
}
private Result(T data) {
this.data = data;
}
private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
private Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
private Result(CodeMsg codeMsg) {
if (codeMsg != null) {
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

状态码

package com.liuyanzhao.sens.result;
/**
* 状态码,错误码
* @author liuyanzhao
*/
public class CodeMsg {
private int code;
private String msg;
//通用的错误码
public static CodeMsg SUCCESS = new CodeMsg(200, "成功");
public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
public static CodeMsg REQUEST_ILLEGAL = new CodeMsg(500102, "请求非法");
public static CodeMsg ACCESS_LIMIT_REACHED = new CodeMsg(500103, "访问太频繁!");
//登录模块 5002XX
public static CodeMsg USER_NOT_LOGIN = new CodeMsg(500200, "用户未登录");
public static CodeMsg TOKEN_INVALID = new CodeMsg(500201, "token无效");
public static CodeMsg USERNAME_NOT_EXIST = new CodeMsg(500202, "用户名不存在");
public static CodeMsg PASSWORD_ERROR = new CodeMsg(500203, "密码错误");
private CodeMsg() {
}
private CodeMsg(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public CodeMsg fillArgs(Object... args) {
int code = this.code;
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
}
@Override
public String toString() {
return "CodeMsg ";
}
}

四、统一异常处理

自定义统一异常

package com.liuyanzhao.sens.exception;
import com.liuyanzhao.sens.result.CodeMsg;
/**
* 统一异常
*/
public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 1L;
private CodeMsg cm;
public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}
public CodeMsg getCm() {
return cm;
}
}

统一异常处理类

package com.liuyanzhao.sens.exception;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.liuyanzhao.sens.result.CodeMsg;
import com.liuyanzhao.sens.result.Result;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 统一异常处理
*/
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(HttpServletRequest request, Exception e) {
e.printStackTrace();
if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return Result.error(ex.getCm());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
List errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
} else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}

五、Redis 封装类

package com.liuyanzhao.sens.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author 言曌
* @date 2018/12/16 下午6:57
*/
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate redisTemplate;
// Key(键),简单的key-value操作
/**
* 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
*
* @param key
* @return
*/
public long ttl(String key) {
return redisTemplate.getExpire(key);
}
/**
* 实现命令:expire 设置过期时间,单位秒
*
* @param key
* @return
*/
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:INCR key,增加key一次
*
* @param key
* @return
*/
public long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 实现命令:key,减少key一次
*
* @param key
* @return
*/
public long decr(String key, long delta) {
if(delta<0){
// throw new RuntimeException("递减因子必须大于0");
del(key);
return 0;
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
*/
public Set keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 实现命令:DEL key,删除一个key
*
* @param key
*/
public void del(String key) {
redisTemplate.delete(key);
}
// String(字符串)
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout (以秒为单位)
*/
public void set(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*
* @param key
* @return value
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
// Hash(哈希表)
/**
* 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
*
* @param key
* @param field
* @param value
*/
public void hset(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
/**
* 实现命令:HGET key field,返回哈希表 key中给定域 field的值
*
* @param key
* @param field
* @return
*/
public String hget(String key, String field) {
return (String) redisTemplate.opsForHash().get(key, field);
}
/**
* 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*
* @param key
* @param fields
*/
public void hdel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields);
}
/**
* 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
*
* @param key
* @return
*/
public Map hgetall(String key) {
return redisTemplate.opsForHash().entries(key);
}
// List(列表)
/**
* 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long lpush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 实现命令:LPOP key,移除并返回列表 key的头元素。
*
* @param key
* @return 列表key的头元素。
*/
public String lpop(String key) {
return (String) redisTemplate.opsForList().leftPop(key);
}
/**
* 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long rpush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
}

代码GitHub地址:https://github.com/saysky/sensboot

Java帮帮

非盈利学习社区

官网:www.javahelp.com.cn

51cdab475b93cfb7619a44d9dc234419.png

职涯宝

时间宝贵,阅读价值

官网:www.zhiya360.com

459b77a224d136f7d86c3271a5c796ef.png

九点编程

深夜学习,未来可期

22cb708c31d278cb22105da50adefc3d.png

悟空教程

强大自己,立于不败

f3eaa55260121afbfee94ce878f16313.png

Python帮帮

人工智能,不得不看

7b41f40e9a0952b815db673425982064.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值