PS:带删除线的方法
可以使用但是不建议使用(方法提供方说的)
加上@Deprecated注解。就代表这个方法可以使用,但是不建议使用。也就会带删除线了
前言
对于用户登录。
我们之前的做法都是
1.用户登录,后端验证用户名和密码正确,则存储Session中。把SessionId存储在Cookie中
2.用户再次访问的时候,后端从Cookie中获取SessionId。根据SessionId获取Session。
存在问题:
1.Session丢失:Session存储在服务器内存中,如果服务器重启。那么Session就丢失了
如果用户刚登录成功。服务器进行重启Session丢失。客户端就需要重新登陆了。
2.多机部署的情况 :如果是单机,这台机器只要挂掉(①机器出现问题/②修改代码后服务器重启)。整个服务就挂掉。因此公司通常多机部署。
假如现在有一个客户端,和三个服务器。
①用户登录,用户请求到了服务器1。服务器1存储Session。
②用户访问。请求到了服务器2。服务器2根据用户的SessionId查找Session。但找不到。就会告诉用户未登录。这就出现了bug。
解决办法:
1.数据共享,把Session放在同一个地方。比如redis。
2.把数据放在客户端上。(类似身份证,由公安机关发放。用于每个人的身份校验。(我们把这个“身份证”就称作token令牌))
服务器具备生成令牌和验证令牌的能力
使用令牌技术后
1.用户登录,用户发起登录请求, 经过负载均衡, 把请求转给了第一台服务器, 第一台服务器进行账号密码验证, 验证成功后, 生成一个令牌, 并返回给客户端.
2.客户端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(比如localStorage)
3.查询操作,用户登录成功之后, 携带令牌继续执行查询操作, 比如查询博客列表. 此时请求转发到了第二台机器, 第⼆台机器会先进行权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明用户已经执行了登录操作, 如果令牌是无效的, 就说明用户之前未执行登录操作.
我们将token。也称作令牌。
令牌的优缺点
优点:
解决了集群环境下的认证问题。
减轻服务器的存储压力(无需在服务器存储)
缺点:
需要自己实现,包括令牌的生成、令牌的传递、令牌的校验。
一、JWT令牌(一种流行的公共令牌技术)
1.1JWT令牌简介
全称: JSON Web Token(官网)
token令牌其实就是一个字符串。用于校验用户身份。
对上⾯部分的信息, 使用Base64Url 进行编码, 合并在一起就是jwt令牌Base64是编码方式,而不是加密方式 。
简介:
JWT由三部分组成、每部分中间使用(.)分隔。
①Header(头部):包括令牌类型(JWT)、以及使用的哈希算法(如HMAC、SHA256、RSA)
②Payload(负载):负载部分是存放有效信息的地方。里面是一些自定义内容比如。{"userId":"123","userName":"zhangsan"}。也可以存在jwt提供的现场字段, 比如exp(过期时间戳)等.此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
③Signature(签名):此部分用于防止jwt内容被篡改, 确保安全性。
注:防止被篡改, 而不是防止被解析.
WT之所以安全, 就是因为最后的签名. jwt当中任何一个字符被篡改, 整个令牌都会校验失败.
好比我们的身份证, 之所以能标识一个⼈的⾝份, 是因为他不能被篡改, 二不是因为内容加密.(任何人都可以看到身份证的信息, jwt 也是)
1.2JWT令牌的使用
1.2.1引入依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred--><version>0.11.5</version><scope>runtime</scope></dependency>
1.2.2使用Jar包中提供的API来完成JWT令牌的生成和校验
1.在Test中创建JWTUtilsTest类
如果需要从Spring容器中获取一些信息。则加上SpringBootTest注解。
而我们这里不用。
定义常量
//过期时间:设置为一小时后过期private final static long EXPIRATION_DATE = 60 * 1000;private final static String secretString = "sqmbcvcBPjfvJ4ilRLqbGmHeUaCEwdpv10jSRbCNtH4=";private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));//生成key
生成token令牌
@Testpublic void gentToken(){Map<String,Object> claim = new HashMap<>();claim.put("id",5);claim.put("name","王五");String token = Jwts.builder().setClaims(claim) //设置头部和荷负载.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE)).signWith(key) //设置签名.compact();System.out.println(token);}
随机生成key(标签)
/*** 随机生成Key.(标签)*/@Testpublic void genKey(){SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);String encode = Encoders.BASE64.encode(secretKey.getEncoded());System.out.println(encode);}
解析Token
/*** 解析Token*/@Testpublic void parseToken(){String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi546L5LqUIiwiaWQiOjUsImV4cCI6MTczMTQ3MDYxMX0.s-6fv04cAt_8IY-BDScfzzqq-XtuEZ4THuqj_ekw824";JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims body = build.parseClaimsJws(token).getBody();System.out.println(body);}
二、登录接口的实现(使用JWT令牌技术)
2.1在utils包创建JWTUtils类
@Slf4j
public class JWTUtils {//过期时间:设置为一小时后过期private final static long EXPIRATION_DATE = 60 * 60 * 1000;private final static String secretString = "sqmbcvcBPjfvJ4ilRLqbGmHeUaCEwdpv10jSRbCNtH4=";private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));//生成key/*** 生成token令牌*/public static String gentToken(Map<String,Object> claim){return Jwts.builder().setClaims(claim) //设置头部和荷负载.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE)).signWith(key) //设置签名.compact();}/*** 随机生成Key.(标签)* 由于我们已经生成了因此不需要这个代码了*/
// public void genKey(){
// SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// String encode = Encoders.BASE64.encode(secretKey.getEncoded());
// System.out.println(encode);
// }/*** 解析Token*/public static Claims parseToken(String token){Claims body = null;JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();try {body = build.parseClaimsJws(token).getBody();} catch (ExpiredJwtException e) {log.info("token过期,校验失败,token:{}",token);} catch (Exception e) {log.info("token校验失败,token:{}",token);}return body;}public static boolean checkToken(String token){Claims body = parseToken(token);if(body == null){return false;}return true;}
2.2实现后端接口
@Autowiredprivate UserService userService;@RequestMapping("/login")public Result login(String userName, String password){//1.参数校验//2.对密码进行校验//3.如果校验成功,生成tokenif(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){return Result.fail("用户名或密码不能为空!");}UserInfo userInfo = userService.queryUserByName(userName);if(userInfo == null || userInfo.getId() <= 0){return Result.fail("用户不存在");}if(!password.equals(userInfo.getPassword())){return Result.fail("密码错误!");}//密码正确Map<String,Object> claim = new HashMap<>();claim.put("id",userInfo.getId());claim.put("name",userInfo.getUserName());return Result.success(JWTUtils.gentToken(claim));}
2.3实现前端接口
客户端可以把Token放在哪里呢?
1.Cookie(推荐,但实现较复杂)
2.本地存储(推荐,实现简单)(下面代码使用这个)
3.url中(一般不这样用)
使用
localStorage.setItem("user_token",result.data)
存储token。登录后按f12。点击应用程序,找到本地存储。就能看到我们存储的token了
function login() {$.ajax({type: "post",url: "user/login",data: {"userName": $("#username").val(),"password": $("#password").val()},success:function(result){if(result.code == 200 && result.code != null){//存储TokenlocalStorage.setItem("user_token",result.data)location.href = "blog_list.html"}else if(result.errMsg == "用户名或密码不能为空!"){alert("用户名或密码不能为空!");}else if(result.errMsg == "用户不存在"){alert("用户不存在");}else if(result.errMsg == "密码错误!"){alert("密码错误!");}//不为200如何处理.....}});}
成功登录!
三、强制登录(拦截器)
1.客户端访问时,携带token(token通常放在Header中)
2.服务器获取token,验证token,如果token校验成功,放行。否则跳转到登录页面。
客户端返回token
在定义拦截器之前。我们需要从客户端获得token。若token存在且校验正确。那么放行。不然进行拦截
$(document).ajaxSend(function(e,xhr,opt){var user_token = localStorage.getItem("user_token")xhr.setRequestHeader("user_token_header",user_token)
});
//放在common.js中,这时候所有引入common.js的页面都会执行这个代码。
//放在common.js中,这时候所有引入common.js的页面都会执行这个代码。
每当发起ajax请求。就会执行这个方法。ajaxSend
这样我们向后端发送token。将这个变量名命名为user_token_header。
登录状态失效(提示后跳转到登录状态)
放在common.js中,这时候所有引入common.js的页面都会执行这个代码。
每当发起ajax请求。如果请求发生错误。就会执行这个方法。ajaxError
$(document).ajaxError(function(event, jqxhr, settings, thrownError) {// 检查是否是未授权错误if (jqxhr.status === 401) {alert("登录已失效,请重新登录!");window.location.href = "/blog_login.html";} else {// 处理其他错误console.error("AJAX 请求出错,状态码:", jqxhr.status);alert("请求出错,请稍后再试!");}
});
3.1自定义拦截器
1.创建interceptor包。创建LoginInterceptor类,加上@Component注解
2.实现HandlerInterceptor接口,
3.重写preHandle方法{
//1.从handler中获取请求 //2.校验token //3.成功放行
}
/*** 用户登录拦截器*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.从handler中获取请求//2.校验token//3.成功放行String userToken = request.getHeader("user_token_header");log.info("获得token,token:{}",userToken);boolean result = JWTUtils.checkToken(userToken);if(result){return true;}response.setStatus(401);return false;}
}
3.2注册拦截器配置
1.创建config包。创建WebConfig类 implements WebMvcConfigurer。
2.注册拦截器
3.放入拦截内容以及不拦截的内容
/*** 注册拦截器并配置拦截路径*/
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/**/*.html","/blog-editormd/**","/css/**","/js/**","/pic/**","/user/login");}
}
1.注意对应关系,这两个名称可以一样。
我们完全可以写成一样的。
2.common.js的引用。必须放在jQuery.min.js的下面
ajaxSend是基于JQuery实现的。因此common.js的引用。必须放在
jQuery.min.js的下面。如果调整顺序。会导致运行不成功。
<script src="js/jquery.min.js"></script><script src="js/common.js"></script>
四、获得当前用户信息接口
根据token,获得用户信息
后端代码
controller
@RequestMapping("getUserInfo")public UserInfo getUserInfo(HttpServletRequest request){//1.获取token,从token中获取Id//2.根据Id,获得用户信息String user_token = request.getHeader(Constant.USER_TOKEN_HEADER);Integer userId = JWTUtils.getUserIdFromToken(user_token);if(userId == null || userId<=0){return null;}return userService.queryUserById(userId);}
Service
public UserInfo queryUserById(Integer userId) {return userMapper.selectById(userId);}
Mapper
@Select("select *from user where id = #{userId} and delete_flag = 0")UserInfo selectById(Integer userId);
测试的时候。注意加上Header信息。也就是token。
我们发现password返回不太合适。因此可以进行处理。
@RequestMapping("getUserInfo")public UserInfo getUserInfo(HttpServletRequest request){//1.获取token,从token中获取Id//2.根据Id,获得用户信息String user_token = request.getHeader(Constant.USER_TOKEN_HEADER);Integer userId = JWTUtils.getUserIdFromToken(user_token);if(userId == null || userId<=0){return null;}UserInfo userInfo = userService.queryUserById(userId);userInfo.setPassword("");return userInfo;}
企业开发中并不会出现这种情况。
企业中。接口返回的实体类是单独定义的。并不是我们项目中使用的那个,
通过解耦的思想。我们返回的接口数据。并不是Userinfozhong
比如接口中返回用户信息。会重新定义一个比如UserInfoApi。
这个实体类。和UserInfo是对应的。UserInfoApi 是接口需要什么。设置什么
而UserInfo是与数据库对应的。
前端代码
<div class="container"><div class="left"><div class="card"><img src="pic/doge.jpg" alt=""><h3></h3><a href="#">GitHub 地址</a>
getUserInfo();function getUserInfo(){$.ajax({type: "get",url: "/user/getUserInfo",success:function(result){if(result.code == 200 && result.data!=null){$(".left .card h3").text(result.data.userName);$(".left .card a").attr("href",result.data.githubUrl);}}});}
".left .card a"
.card前面的空格一定要加上
成功显示
五、 获取作者信息接口
根据博客Id。获取作者Id
根据作者Id。获取作者信息。
后端代码
Controller
@RequestMapping("/getAuthorInfo")public UserInfo getAuthorInfo(Integer blogId){//1.根据博客Id,获取作者Id//2.根据作者Id,获取作者信息if(blogId != null && blogId< 1){return null;}UserInfo authorInfoByBlogId = userService.getAuthorInfoByBlogId(blogId);authorInfoByBlogId.setPassword("");return authorInfoByBlogId;}
Service
public UserInfo getAuthorInfoByBlogId(Integer blogId) {//1.根据博客Id,获取作者Id//2.根据作者Id,获取作者信息BlogInfo blogInfo = blogMapper.selectById(blogId);if(blogInfo ==null || blogInfo.getUserId()<1){return null;}return userMapper.selectById(blogInfo.getUserId());}
Mapper
@Select("select *from user where id = #{userId} and delete_flag = 0")UserInfo selectById(Integer userId);
使用Postman测试一下
前端代码
<div class="container"><div class="left"><div class="card"><img src="pic/doge.jpg" alt=""><h3></h3><a href="#">GitHub 地址</a>
//显示博客作者信息
/* var userUrl = "/user/getAuthorInfo" + location.search;getUserInfo(userUrl); */getUserInfo();function getUserInfo(){$.ajax({type: "get",url: "/user/getAuthorInfo"+location.search,success:function(result){if(result.code == 200 && result.data!=null){$(".left .card h3").text(result.data.userName);$(".left .card a").attr("href",result.data.githubUrl);}}});}
最终成功显示
代码整合
我们发现四、五这两个接口的前端代码几乎一样。
因此我们可以把这个代码放在common.js文件中。
function getUserInfo(url){$.ajax({type: "get",url: url,success:function(result){if(result.code == 200 && result.data!=null){$(".left .card h3").text(result.data.userName);$(".left .card a").attr("href",result.data.githubUrl);}}}); }
接着在前端响应的接口处调用这个方法。传入参数就行了。