导航
- 常见的登录实现方式
- session工作机制
- 基于Springoot中登录流程
- 最简单的代码实现
- 多条腿走路-共享session
- 结语
- 参考
常见的登录实现方式
- cookie
1)用户登录认证之后,将用户信息保存在本地浏览器中cookie(还可以设置过期时间),
2)后面每次发起http请求,都自动携带上该信息,就能达到认证用户,保持用户在线。
- cookie+session
1)用户登录成功之后,在服务端就会生成一个键值对。key叫做sessionid,value就保存ssession(用户信息),客户端那边就需要把sessionid存储到cookie。
2)后续的http请求会携带上sessionid,服务器就根据sessionid来查找对应的信息。
- token
1)用户登录成功之后,token 由服务器本身根据算法生成后下发给客户端,服务器端无需额外存储。
2)客户端请求服务器时,在请求头中追加携带该token
3)服务器端对token进行验签,从而决定本次访问是拒绝还是放行。
本章节主要是基于session实现登录。
session 工作机制
1.用户向服务器提交认证信息
2.服务器认证通过后,在当前对话(session)里面保存相关数据,sessionid,session;
3.服务器向用户返回一个sessionid, 写入用户的cookie,set-cookie
4.用户后续的每一次请求, 都会通过cookie,将sessionid传回服务器
5.服务器端收到sessionid, 找到前期保存的数据,验证用户的身份
我们可以在在google浏览器中,直观地查看sessionId的存储和传输:
基于Springoot中登录流程
掌握了session工作机制之后,我们就可以进行登录流程的设计了。
整个登录流程相对比较复杂,有首次登录,登录成功和失败等多种情况。
我们将其中正常登录的的核心部分梳理出来,便于我们更加纯粹的理解用户首次登录之后,前后端的交互流程。
这幅图非常清晰地展示了浏览器和服务端的交互流程。
但是,从代码的实施的角度来看,光有这个图还是不够完备,我们需要一个更加全面而细化的流程图来指导我们完成登录功能的代码实现。
有了这流程图,基本上考虑到用户登录业务流程,包括了请求(路由)拦截的过滤,首次和N次登录的不同分支等通用逻辑。
最简单的代码实现
当我们彻底理解了登录的流程之后,代码实施就是水到渠成的事情。
在superblog
项目中,我们通过实现登录拦截器来实现用户登录拦截并验证。
SpringBoot通过实现HandlerInterceptor
接口实现拦截器,通过实现WebMvcConfigurer
接口实现一个配置类,在配置类中注入拦截器,最后再通过@Configuration
注解注入配置。
实现HandlerInterceptor接口
主要是实现HandlerInterceptor接口的3个方法: preHandle、postHandle、afterCompletion。
Notes: 关于Spring拦截器的知识在前面章节《深入浅出Spring拦截器》已经阐述。
public class AuthHandlerInterceptor implements HandlerInterceptor {
/**
* 登录拦截器
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/***
* controller权限控制
*/
// 如果请求的不是方法 则直接跳过当前拦截器
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取当前请求的方法
Method method = handlerMethod.getMethod();
// 获取方法上注解
EnableAuth auth = method.getAnnotation(EnableAuth.class);//需要验证
IgnoreAuth ignoreAuth=method.getAnnotation(IgnoreAuth.class);//跳过验证
//如果跳过验证,则返回
if(ignoreAuth!=null)
return true;
// 如果方法没有注解,则去类上去获取 如果方法有则使用方法的注解,方法的注解优先级高于类的注解
if (auth == null) {
auth = handlerMethod.getBeanType().getAnnotation(EnableAuth.class);
}
// 如果没有Auth 注解 则跳过鉴权
if (auth == null) {
return true;
}
Object username = request.getSession().getAttribute("aname");
if (username != null) {
//已登录放行
return true;
} else {
// 未登陆,返回登录页面
request.setAttribute("msg","无权限请先登录");
request.getRequestDispatcher("/account/login").forward(request, response);
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
preHandle在请求进入Controller之前执行,因此拦截器的功能主要就是在这个部分实现。代码有详细的注释,不再赘述。
注册拦截器
增加WebMvcConfig
配置类,实现WebMvcConfigurer
接口来,将上面实现的拦截器的一个对象注册到这个配置类中。
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 注册登录拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthHandlerInterceptor()).addPathPatterns("/**")//所有请求路径都被拦截
.excludePathPatterns(//不拦截的请求路径
"/",
"/account/login",
"/account/gologin",
"/account/register",
"/assets/**",
"/css/**",
"/font/**",
"/font-awesome4.0.3/**",
"/js/**",
"/images/**",
"/avatars/**",
"/img/**");
}
}
controller,service层的实现
AccountController.cs
@Controller
@RequestMapping("/account")
public class AccountController {
@Resource
@Autowired
private AdminUserService adminUserService;
@RequestMapping("/login")//主页
public String index(){
return "login";
}
@RequestMapping("/gologin")//登录获取用户信息存到session
public String gologin(
LoginRequest loginRequest,
HttpServletRequest request,
Map<String,Object> map
){
//对密码进行 md5 加密
String md5Password = DigestUtils.md5DigestAsHex(loginRequest.getPassword().getBytes());
boolean isOk=adminUserService.login(loginRequest.getAccount(),md5Password);
if(!isOk)
{
return "login";
}
HttpSession session = request.getSession();
session.setAttribute("aname",loginRequest.getAccount());
//重定向
return "redirect:/default/index";
}
/* 注销登录
* @param session
* @return*/
@RequestMapping(value = "logout",method = RequestMethod.GET)
public String logout(HttpSession session){
session.invalidate();//使Session变成无效,及用户退出
return "redirect:/account/login";
}
}
AdminUserServiceImpl.cs
@Service
public class AdminUserServiceImpl extends ServiceImpl<AdminuserMapper,Adminuser> implements AdminUserService {
@Autowired
private AdminuserMapper adminuserMapper;
@Override
public boolean login(String account,String passwordsalt)
{
QueryWrapper<Adminuser> queryWrapper=new QueryWrapper<>();
queryWrapper.lambda().eq(Adminuser::getAccount, account)
.eq(Adminuser::getPasswordSalt,passwordsalt);
return adminuser!=null;
}
}
这样,我们最基础的web站点登录就实现了。
多条腿走路-共享session
在单机环境下,Session是存储在应用服务的内存中,就就像上面的代码实现。
将设一个场景,用户此时正在访问你的网站,而网站是单击部署的。当我们发版的时候,应用程序会重启,导致用户存储的session销毁,然后用户就会重新登录。
这个体验并不友好,有解决的办法吗?
我们可以实现分布式部署,并将session存储到别的地方,实现sesssion共享。
这里使用spring-session实现分布式环境下Session共享方案,Session信息存储在redis中。
引入依赖
<!-- 引入 redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--用于使用redis接管session-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
redis节点配置
## redis
#session存储类型
spring.session.store-type=redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.timeout=3000
Notes: 请提前安装redis-server.
为了测试方便,我在本机安装RedisDesktopManager,可以用可视化的界面查看redis的数据。
启动项目,再次登录
重新登录之后,然后我们停止了程序。
查看redis,我们看到session已经写入了redis。
结语
本章节,首先梳理了基于sesssion机制实现的登录流程。
然后以springboot实战案例完成了代码的实现。
这个案例比较简单,希望能够起到抛砖引玉的效果。
由于篇幅限制,视图层和控制器层的源码没有全部展示出来,完整源码请参考《Springboot实战纪实源码》 。