【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录
【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录
上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起。并且通过 axios 发送请求到后端API。
解决跨域问题后、成功从后端获取到数据。
本小结,将和大家一起搭建 Spring-Security + token 的方式先完成登录。权限将在后面讲解。
引入
在之前,我们的 API 都是一种裸奔的方式。谁都可以访问,肯定是不安全的。所以我们要引入安全校验框架。
传统 session 方案
传统session 的方式是,通过一个 拦截器 拦截所有的请求,若 cookie 当中存储的 session id 在服务端过期后、则要求前端重新登录,进而获取一个新的session
session 与 cookie 区别
因为HTTP 是一种无状态的协议。所以服务端不知道这个 请求是谁发过来的,有好多人访问服务器,但是对于服务器来说,这些人我都不认识。就需要一种东西来给每个人加一个 ID 。
session(会话) 是一种客户端发起请求后, 服务端用来识别用户的东西,可以保存一些用户的基本信息。比如ID什么的
cookie 是一种客户端浏览器用来记录和保存信息的东西。简单理解,如图所示。
当然,默认的cookie 里面总会包含一串 JSESSIONID
session认证所显露的问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
JWT
肯定是原有的session认证的方式存在弊端、我们就需要采取一种新的方式来进行验证。JWT
JWT token 由三部分构成:
- 头部(header)
- 载荷(playload)
- 签证(signature)
具体的内容可以参考: https://www.jianshu.com/p/576dbf44b2ae
头部 header
头部一般包含加密算法和类型。例如
{
"alg": "HS256",// 加密算法
"typ": "JWT" // 声明类型
}
负荷 playload
负载可以理解为存放信息的位置,例如:
{
"iss":"mall-pro", // 签发者
"sub":"admin", // 面向的用户
"iat": 1602737566890,//签发时间
"exp": 1602739566890//过期时间,必须大于签发时间
}
签证(signature)
签证一般是头部和负荷组成内容的,一旦头部和负荷内容被篡改,验签的时候也将无法通过。
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
我们来参考一个生成的 JWT 实例
注意,我这里使用回车、一般三部分都是通过标点进行分割的。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
实现原理
- 用户调用登录接口后、验证用户名和密码。验证成功后、颁发给其
token - 前台获得
token后,将其存放到本地、每次的请求都将这个token携带到请求头里面。 - 后台收到请求后、验证请求头里面的
Authorization是否正确、从而判断是否可以调用这个接口。 - 通过解析
token将账号信息存入userDetail让其顺利调用接口信息、并可以在接口中获得当前登录人的账号信息。
Spring Security
安全框架,我们这里考虑使用 Spring-Security ,使用全家桶系列,一般大家都会想到apache shiro 等权限框架、都是可以的。我们这里介绍如何加入 Spring-Security
引入到 mall-security 并且添加一个配置文件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加一个登陆接口
我们首先从登陆接口开始,一个最基本的 controller 接受参数。当然,用户名和密码肯定是不能为空的,校验完后交给 service
@ApiOperation("用户登录接口")
@RequestMapping("login")
public CommonResult login(@RequestBody @Valid @ApiParam("用户名密码") UmsAdminLoginParam param) {
UmsAdminTokenBO tokenBO = umsAdminService.umsAdminLogin(param);
return CommonResult.success(tokenBO);
}
具体的内容无非是:查询数据库、是否存在、密码是否正确。正确就构造一个 token 返回给前端。这里主要说一些重要的点。
断言与全局异常处理
断言可以理解为:若当前行不符合判断条件、则抛出异常。或者直接使用断言来抛出一个异常。比如账号不存在,直接抛出一个异常即可。
全局异常处理:全局异常处理,在全局统一拦截异常信息,并通过
{code=500,message="error message"}的方式返回给前端做出提示即可。
Springboot 对于全局异常的处理、简直是简单的不得了~
@RestControllerAdvice
@Slf4j
public class GlobalExControllerHandler {
/**
* <p>全局异常拦截器,拦截自定义ApiException
* <p>author: <a href='mailto:maruichao52@gmail.com'>MRC</a>
*
* @param e 自定义异常
* @return xyz.chaobei.common.api.CommonResult
* @since 2020/10/20
**/
@ExceptionHandler(value = ApiException.class)
public CommonResult exceptionHandler(ApiException e) {
log.info("系统异常拦截器:异常信息:" + e.getMessage());
if (Objects.nonNull(e.getErrorCode())) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
直接通过 return 的方式,就好像我们在 controller 里面给前端返回json 一样简单。
断言则是,判断某一条件是否成立、如果不成立则抛出异常的一种更加简单的方式。就不用每次都写throw new xxxException
简而言之就是:一种非常优美的方式抛异常(偷懒的)
public class Asserts {
/**
* <p>断言抛出一个异常
* <p>author: <a href='mailto:maruichao52@gmail.com'>MRC</a>
*
* @param message 提示语
* @return void
* @since 2020/10/15
**/
public static void fail(String message) {
throw new ApiException(message);
}
public static void fail(IErrorCode iErrorCode) {
throw new ApiException(iErrorCode);
}
}
Spring Security UserDetails
Spring UserDetails 作为一个接口、规定了一些需要的参数方法。我们必须要用自己的逻辑实现这个方法。并将username password 等重要信息通过其定义的方法进行返回。也是作为一种桥接、将我们的用户名、密码等信息交付给 SpringSecurity
public class UmsAdminUserDetails implements UserDetails {
private final UmsAdminModel adminModel;
public UmsAdminUserDetails(UmsAdminModel adminModel) {
this.adminModel = adminModel;
}
// 省略,具体请查看源码
}
JWT 签发服务
JWT 又称作JsonWebToken ,我们需要一个依赖来生成token/登录后需要将这个 token 返回给前端,让前端保存,而后所有的请求都需要带上这个 token 然后我们服务端就知道是哪个用户在请求了。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
生成token
我在上面的内容里面已经介绍了。我们的token 必须要包含:
sub签发给谁iat过期时间戳iss谁签发的
/**
* 功能描述: 通过负载生成token
*
* @Param: claims 负载
* @Return: java.lang.String
* @Author: MRC
* @Date: 2020/10/21 0:17
*/
private String buildToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret())
.compact();
}
通过builder() 构造器、设置其负载内容、并且指定 过期时间setExpiration ,以及加入秘钥进行加密 signWith
token 检验
token 检验包含:当前token 是否有效(能顺利从token取出我们的sub)、以及检验其是否过期 无效等。
/**
* <p>从toKen中获取负载信息
* <p>author: <a href='mailto:maruichao52@gmail.com'>MRC</a>
*
* @param token 获取的token
* @return io.jsonwebtoken.Claims
* @since 2020/10/22
**/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.info("JWT格式验证失败:{}", token);
}
return claims;
}
该方法描述了如何从一个token 里面取出我们所需要的 Claims 信息。并且可以从负载里面取出 sub 以及 exp 等信息。我简要介绍一个。其他的详细内容请查看源码。
/**
* <p>首先获取token当中的负载、而后从负载中取出sub
* <p>author: <a href='mailto:maruichao52@gmail.com'>MRC</a>
*
* @param token 被校验的token
* @return java.lang.String
* @since 2020/10/22
**/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
如果你的token被篡改了,那么验证的时候肯定会报错、所以要捕获一下异常。返回空即可。
login service
写到这里,我们login 控制器的service 已经可以全部写下去了。登录成功,通过tokenService 返回一个token ,然后封装返回给前端即可。
@Override
public UmsAdminTokenBO umsAdminLogin(UmsAdminLoginParam param) {
// 通过用户名获取userDetail
UserDetails userDetails = this.findUserDetailByUserName(param.getUsername());
// 基本校验用户名和密码
if (!passwordEncoder.matches(param.getPassword(), userDetails.getPassword())) {
Asserts.fail("用户名密码错误");
}
// 这里暂时不开启权限,后面再修改
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null);
// 将构建的用户信息加入spring security context 上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = defaultTokenServer.generateToken(userDetails);
return UmsAdminTokenBO.builder().token(token).tokenHeader(jwtConfig.getTokenHeader()).build();
}
Security Config
接下来。就是配置一个全局的Security Config
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
主要还是需要重写configure() 方法。获取一个 registry 实例。将我们的拦截信息加入到里面。
- 配置开放的路径
- 配置需要验证的路径。
- 添加一个JWT默认过滤器,在
SpringSecurity处理之前,将token 进行校验后加入到context上下文里面。
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
// 添加开放的路径
for (String url : urlsConfig.getUrls()) {
registry.antMatchers(url).permitAll();
}
// 允许跨域预请求
registry.antMatchers(HttpMethod.OPTIONS).permitAll();
// 所有的请求都需要身份认证
registry.and()
.authorizeRequests()
.anyRequest().authenticated()
// 关闭csrf 不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝
.and()
.exceptionHandling()
.accessDeniedHandler(this.customerAccessDenied())
.authenticationEntryPoint(this.customerAuthentication())
// 添加权限拦截器和JWT拦截器,注意,是before
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
自定义过滤器
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtConfig jwtConfig;
@Autowired
private DefaultTokenServer defaultTokenServer;
@Autowired
private UserDetailsService userDetailsService;
/**
* <p> token 过滤器逻辑
* 1、token 必须存在
* 2、toKen 必须正确,未过期。
* 3、若上下文不存在。则往上下文放一个userDetail
* <p>author: <a href='mailto:maruichao52@gmail.com'>MRC</a>
*
* @param request 请求
* @param response 响应
* @param filterChain 过滤器
* @return void
* @since 2020/10/22
**/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(jwtConfig.getTokenHeader());
log.info("doFilterInternal request url={}", request.getRequestURL());
log.info("doFilterInternal request token={}", token);
// 请求携带token/则检验这个token是否正确和是否过期
if (!StringUtils.isEmpty(token)) {
// 携带的用户名信息
String username = defaultTokenServer.getUserNameFromToken(token);
log.info("request token username={}", username);
if (StringUtils.isEmpty(username)) {
filterChain.doFilter(request, response);
}
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//校验token是否有效
if (defaultTokenServer.isTokenExpired(token)) {
filterChain.doFilter(request, response);
}
//检查当前上下文是否存在用户信息,若没有则添加
if (SecurityContextHolder.getContext().getAuthentication() == null) {
log.info("doFilterInternal getContext = null");
// 将用户信息添加到上下文。说明这个request 是通过的。
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("doFilterInternal user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// 通过拦截器
filterChain.doFilter(request, response);
}
}
其实我们这里去掉session 以后,我们的客户端对于前端的请求标识、只能通过携带token的方式。
然后我们每一个请求首先会进入JwtAuthenticationTokenFilter 也就是我们上面写的这个。
检查当前请求有没有携带token 要是带了 token 那就检查它,检查成功就从数据库查出来这个人。把这个人注入到我们的SpringSecurity Context 里面。
SpringSecurity 的其他过滤器看到上下文有东西在,就放行~说明是登录后的。
要是没带、或者验证错误~。那上下文也就没有这个用户的信息了。所以这个请求只能返回403
密码问题
这里使用的是:PasswordEncoder 接口实现类下的 BCryptPasswordEncoder ,当然,你肯定要在使用之前要用@Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
未来使用的时候、直接注入一个就行了。
matches校验encode加密
至于是怎么加密的。当然还得研究一下~
实际测试
在未登录之前,我们访问一个接口~
{
"code": 401,
"data": "Full authentication is required to access this resource",
"message": "暂未登录或token已经过期"
}
首先使用用户名和密码进行登录,我们加入一条数据。admin,123456
INSERT INTO `mall-pro`.`ums_admin`(`id`, `username`, `password`, `icon`, `lock`, `email`, `nick_name`, `note`, `create_time`, `login_time`, `status`) VALUES (1, 'admin', '$2a$10$08arRlZRspTqMBK1N8NqW.9CQq7KWffa47MGelgJMuPK/uXtKX3O6', '#e', 1, 'maruichao@gmail.com', '管理员', '测试', '2020-10-22 16:14:33', '2020-10-22 16:14:36', 1);
请求登录接口/auth/login ,验证用户名和密码后、返回信息如下:
{
"code": 200,
"message": "操作成功",
"data": {
"tokenHeader": "Authorization",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6Im1hbGwtcHJvIiwiZXhwIjoxNjAzNTAzNjU3LCJpYXQiOjE2MDM0MTcyNTc4MzJ9.5bX2gajbRebS9MyII3OlBKD4xc5uTgelvFprT8SHvBq_MnFa--CSn3ntkGteITt5lLRbAyxyzC8u8KZ1ZCdYjg"
}
}
将登录后,将指定头和token带入请求头进行请求,成功请求到数据~
小结
已经好久没更新这一篇文章了。希望我的读者你们不要怪我,实在是太忙了。白天要上班,偶尔摸鱼写一写,代码调试完、而后我再整理这篇文章。现在已经是凌晨00:26 。加油吧~ 我努力更新完这个系列。
源码地址
https://gitee.com/mrc1999/mall-pro
欢迎关注

【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录的更多相关文章
- 【手摸手,带你搭建前后端分离商城系统】01 搭建基本代码框架、生成一个基本API
[手摸手,带你搭建前后端分离商城系统]01 搭建基本代码框架.生成一个基本API 通过本教程的学习,将带你从零搭建一个商城系统. 当然,这个商城涵盖了很多流行的知识点和技术核心 我可以学习到什么? S ...
- 【手摸手,带你搭建前后端分离商城系统】02 VUE-CLI 脚手架生成基本项目,axios配置请求、解决跨域问题
[手摸手,带你搭建前后端分离商城系统]02 VUE-CLI 脚手架生成基本项目,axios配置请求.解决跨域问题. 回顾一下上一节我们学习到的内容.已经将一个 usm_admin 后台用户 表的基本增 ...
- List多个字段标识过滤 IIS发布.net core mvc web站点 ASP.NET Core 实战:构建带有版本控制的 API 接口 ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目 Using AutoFac
List多个字段标识过滤 class Program{ public static void Main(string[] args) { List<T> list = new List& ...
- 利用grunt-contrib-connect和grunt-connect-proxy搭建前后端分离的开发环境
前后端分离这个词一点都不新鲜,完全的前后端分离在岗位协作方面,前端不写任何后台,后台不写任何页面,双方通过接口传递数据完成软件的各个功能实现.此种情况下,前后端的项目都独立开发和独立部署,在开发期间有 ...
- ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目
一.前言 这几年前端的发展速度就像坐上了火箭,各种的框架一个接一个的出现,需要学习的东西越来越多,分工也越来越细,作为一个 .NET Web 程序猿,多了解了解行业的发展,让自己扩展出新的技能树,对自 ...
- 【转】python+django+vue搭建前后端分离项目
https://www.cnblogs.com/zhixi/p/9996832.html 以前一直是做基于PHP或JAVA的前后端分离开发,最近跟着python风搭建了一个基于django的前后端分享 ...
- python drf+xadmin+react+dva+react-native+sentry+nginx 搭建前后端分离的博客完整平台
前言: 经过差不多半年的开发,搭建从前端到服务器,实现了前后端分离的一个集PC端.移动端的多端应用,实属不易,今天得空,好好写篇文章,记录这些天的成果.同时也做个分享. 演示网站地址: http:// ...
- python+django+vue搭建前后端分离项目
以前一直是做基于PHP或JAVA的前后端分离开发,最近跟着python风搭建了一个基于django的前后端分享项目 准备工作:IDE,[JetBrains PyCharm2018][webpack 3 ...
- Aspnet Mvc 前后端分离项目手记(二)关于token认证
在前后端分离的项目中,首先我们要解决的问题就是身份认证 以往的时候,我们使用cookie+session,或者只用cookie来保持会话. 一,先来复习一下cookie和session 首先我们来复习 ...
随机推荐
- 阿里服务器docker部署
首先本人是购买的阿里云服务器,虽然是1g的内存,不过部署一些项目还是没问题的,学生也有一个优惠服务器,好像是70多2g内存的,还是很舒服的,学生党可以试着部署一下,下面呢我就说一下我自己部署的步骤: ...
- 代码检查工具 Sonar 安装&使用
本文主要说明Sonar的安装方式并附上依赖安装包,本文目标只实现本地搭建测试的Sonar环境,以及本地的测试项目的非定制化扫描 本机测试环境:Win10-X64,.vs2017 依赖包: 1 ...
- 关于Apache报错 couldn't perform authentication. AuthType not set!
今天在使用apache搭建yum的web服务时,配置完成后.访问http://ip 时,浏览器报错:500 Internal Server Error 然后查询error.log发现,有如下错误提示: ...
- dubbo学习(三)配置dubbo API方式配置
provider(生产者) import com.alibaba.dubbo.config.ApplicationConfig; import com.alibaba.dubbo.config.Pro ...
- 使用spring mvc拦截器 会话失效处理
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import ...
- spring怎么避免循环依赖
1.循环依赖 (1)概念 对象依赖分为强依赖和弱依赖: 强依赖指的是一个对象包含了另外一个对象的引用,例如:学生类中包含了课程类,在学生类中存在课程类的引用 创建课程类: @Data public c ...
- 详解如何使用koa实现socket.io官网的例子
socket.io官网中使用express实现了一个最简单的IM即时聊天,今天我们使用koa来实现一下利用 socket.io 实现消息实时推送 框架准备 1.确保你本地已经安装好了nodejs和np ...
- springmvc 源码分析(一)-- DisparcherServlet的创建和注册到tomcat
一. servlet 3.0 的使用 1.1 环境搭建: servlet跟spring没有任何关系,我创建一个servlet可以不依赖spring,现在搭建一个纯的servlet项目,并实现简单的类似 ...
- 腾讯一面!说说ArrayList的遍历foreach与iterator时remove的区别,我一脸懵逼
本文基于JDK-8u261源码分析 1 简介 ArrayList作为最基础的集合类,其底层是使用一个动态数组来实现的,这里"动态"的意思是可以动态扩容(虽然ArrayList可 ...
- Centos-切换用户身份-su
su 切换用户身份 相关选项 - 加载相应用户下环境变量 -c 使用某个身份执行一个指令 -m 改变用户身份不改变环境变量 切换为超级用户 su - 普通用户切换为超级用户需要输入密码,超级用户 ...