目录(未完,后见下篇文章)
1. 实现登录
传统做法:
- 登录页面把用户名和密码提交给服务器
- 服务器端验证用户名和密码是否正确,并返回校验结果
- 如果密码正确,在服务器端创建Session并存储,通过Cookie把SessionId返回给浏览器
- 用户再次访问的时候携带着SessionId,后端从Cookie中获取SessionId,根据SessionId获取Session
存在问题:
- Session存储在服务端的内存中,如果服务器重启,session就丢失了。比如:用户刚登录成功,服务器就进行重启,此时Session丢失,客户端需要重新登录,用户体验不好。
- 多机部署的情况下无法直接使用Session。
我们开发的项⽬, 很少会部署在⼀台机器上, 容易发⽣单点故障。(单点故障: ⼀旦这台服务器挂了, 整个应用都没法访问了)。 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡。此时, 来⾃⼀个用户的请求就会被分发到不同的服务器上。
假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:
- 用户登录:用户登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, 把Session存在了第⼀台服务器上。
- 查询操作:用户登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执行查询操作, 比如查询博客列表。此时请求转发到了第⼆台机器, 第⼆台机器会先进行权限验证操作(通过SessionId验证用户是否登录), 此时第⼆台机器上没有该用户的Session, 就会出现问题, 提示用户未登录。
解决办法:
- 数据共享,把Session放到同一个地方,比如redies…
- 把数据放到客户端(令牌技术)
用户身份(token 令牌)存在客户端,服务器可以对用户身份进行校验。用户身份由服务器发放,交给客户端,客户端访问时,携带着身份证,服务器进行校验。(身份证由公安机关发放,存到个人手里,当去景区等地方要验证身份时,拿出身份证,景区通过公安系统对身份进行校验。个人就是客户端,景区就是服务器)
服务器具备⽣成令牌和验证令牌的能力。
我们使用令牌技术, 继续思考上述场景:
- 用户登录:用户登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, ⽣成⼀个令牌, 并返回给客户端。
- 客户端收到令牌之后, 把令牌存储起来。可以存储在Cookie中, 也可以存储在其他的存储空间(比如localStorage)
- 查询操作:用户登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表。此时请求转发到了第三台机器, 第三台机器会先进⾏权限验证操作。服务器验证令牌是否有效, 如果有效, 就说明用户已经执行了登录操作, 如果令牌是⽆效的, 就说明用户之前未执⾏登录操作。
令牌的优缺点:
优点:
- 解决了集群环境下的认证问题
- 减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
1.1 JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现。
JWT组成:
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc
- Header(头部):头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
- Payload(负载):负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容。
比如{“userId”:“123”,“userName”:“zhangsan”} , 也可以存在JWT提供的现场字段, ⽐如exp(过期时间戳)等。此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
- Signature(签名):此部分⽤于防止JWT内容被篡改, 确保安全性。防止被篡改, 而不是防止被解析。
JWT之所以安全, 就是因为最后的签名。JWT当中任何⼀个字符被篡改, 整个令牌都会校验失败。就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密。(任何⼈都可以看到⾝份证的信息, JWT 也是)。
对上⾯部分的信息, 使用Base64Url 进⾏编码, 合并在⼀起就是jwt令牌。Base64是编码方式,⽽不是加密方式。
1.2 JWT令牌的生成和校验
- 引入JWT令牌的依赖
<!-- 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>
- 使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验
生成令牌:
public class JWTUtilsTest {
// 过期时间:1小时的毫秒数
private final static long EXPIRATION_DATE = 60 * 60 * 1000;
// 签名
private final static String secretString = "JxSPPTQPYO5n5jPah/MTzpYcM2Ow4/YbA4i91myjmYg=";
// 生成key
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
// 生成令牌
@Test
public void genToken() {
// 自定义信息
Map<String, Object> claim = new HashMap<>();
claim.put("id",5);
claim.put("name","zhangsan");
String token = Jwts.builder()
.setClaims(claim)// 自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_DATE))// 过期时间
.signWith(key)// 签名算法
.compact();
System.out.println(token);
}
}
注意: 对于密钥key有⻓度和内容有要求, 建议使用
io.jsonwebtoken.security.Keys#secretKeyFor(signaturealgalgorithm)⽅法来创建⼀个密钥
// 生成key
@Test
public void genKey() {
// 随机生成key
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 拿到这个key的签名
String encode = Encoders.BASE64.encode(secretKey.getEncoded());
System.out.println(encode);
}
运行程序:
输出的内容, 就是JWT令牌,通过点(.)对三个部分进⾏分割, 我们把⽣成的令牌通过官⽹进⾏解析, 就可以看到我们存储的信息了:
- HEADER部分可以看到, 使⽤的算法为HS256
- PAYLOAD部分是我们⾃定义的内容, exp表示过期时间
- VERIFY SIGNATURE部分是经过签名算法计算出来的, 所以不会解析
校验令牌:
完成了令牌的⽣成, 我们需要根据令牌, 来校验令牌的合法性(以防客户端伪造)
// 校验令牌
@Test
public void parseToken() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6NSwiZXhwIjoxNzE0MjE1OTkyfQ.apLjuz_lu2lU5U9QPrQyeOKzmGqTTJ-l9aqJyWvSbas";
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();// 创建解析器, 设置签名密钥
// 解析token
Claims body = null;
try {
body = build.parseClaimsJws(token).getBody();
} catch (Exception e) {
System.out.println("令牌校验失败");
}
System.out.println(body);
}
运行程序,查看结果:
令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了。令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败。修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改。
学习令牌的使⽤之后, 接下来我们通过令牌来完成用户的登录:
- 登录页⾯把用户名密码提交给服务器
- 服务器端验证用户名密码是否正确, 如果正确, 服务器⽣成token, 返回给客户端
- 客户端把token存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
- 服务器对token进⾏校验, 如果token正确, 进⾏下⼀步操作
1.3 约定前后端交互接口:
[请求]
/user/login
userName=test&password=123
[响应]
{
"code": 200,
"msg": "",
"data":
"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5N
zg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV--E"
}
//验证成功, 返回token, 验证失败返回错误信息
1.4 实现服务器代码:
创建JwtUtils工具类:
@Slf4j
public class JwtUtils {
// 过期时间:1小时的毫秒数
private final static long EXPIRATION_DATE = 60 * 60 * 1000;
// 签名
private final static String secretString = "JxSPPTQPYO5n5jPah/MTzpYcM2Ow4/YbA4i91myjmYg=";
// 生成key
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
// 生成令牌
public static String genToken(Map<String, Object> claim) {
// 自定义信息
String token = Jwts.builder()
.setClaims(claim)// 自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_DATE))// 过期时间
.signWith(key)// 签名算法
.compact();
return token;
}
// 校验令牌
public static boolean parseToken(String token) {
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();// 创建解析器, 设置签名密钥
// 解析token
Claims body = null;
try {
body = build.parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
log.error("token过期,校验失败,token:",token);
return false;
} catch (Exception e) {
log.error("token校验失败,token:",token);
return false;
}
return true;
}
}
创建 UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 登录接口
@RequestMapping("/login")
public Result login(String userName, String password) {
// 1.校验参数
// 2.对密码进行校验
// 3.如果校验成功,返回token
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return Result.fail("用户名或密码为空");
}
UserInfo userInfo = userService.queryUserByName(userName);
if (userInfo == null || userInfo.getId() < 1) {
return Result.fail("用户不存在");
}
if (!password.equals(userInfo.getPassword())) {
return Result.fail("密码不正确");
}
// 密码正确了
Map<String, Object> claim = new HashMap<>();
claim.put("id", userInfo.getId());
claim.put("userName", userInfo.getUserName());
return Result.success(JwtUtils.genToken(claim));
}
}
创建UserService:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public UserInfo queryUserByName(String userName) {
return userMapper.selectByName(userName);
}
}
运行程序,访问http://127.0.0.1:8080/user/login?userName=zhangsan&password=123456,测试后端接口返回结果是否正常:
1.5 实现客户端代码:
服务器给客户端返回token,客户端可以把token存在哪里呢?
- cookie
- 本地存储
- url
我们使用第二种存储方式。
修改 login.html, 完善登录⽅法,前端收到token之后, 保存在localstorage中:
function login() {
// 发送ajax请求
$.ajax({
type: "post",
url: "/user/login",
data: {
"userName": $("#username").val(),
"password": $("#password").val()
},
success: function(result) {
if (result.code == 200 && result.data != null) {
// 存储token
localStorage.setItem("user_token", result.data);
// 页面跳转
location.href = "blog_list.html";
} else {
// 其他情况
alert("用户名或密码错误");
}
}
});
}
localStorage相关操作:
存储数据:localStorage.setItem(“user_token”,“value”);
读取数据:localStorage.getItem(“user_token”);
删除数据:localStorage.removeItem(“user_token”);
运行程序,访问http://127.0.0.1:8080/blog_login.html,输入用户名和密码,页面跳转:
2. 实现强制要求登录
用户强制登录:
- 客户端访问时,携带token(从localStorage中拿到token,发送http请求时可以放在header或者param中)
- 服务器获取token,验证token,如果token验证成功,就放行
当用户访问博客列表页和博客详情页时, 如果用户当前尚未登录,就⾃动跳转到登录页面。我们可以采⽤拦截器来完成, token通常由前端放在header中, 服务器从header中获取token, 并校验token是否合法。
2.1 添加拦截器
/**
* 用户登录拦截器
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.从header中获取token
// 2.校验token
// 3.成功,放行
String userToken = request.getHeader("user_token_header");
log.info("获得token,token:"+userToken);
boolean result = JwtUtils.parseToken(userToken);
if (result) {
return true;
}
response.setStatus(401);// http状态码
return false;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
.excludePathPatterns(
"/user/login",
"/**/*.html",
"/js/**",
"/css/**",
"/pic/**",
"/blog-editormd/**");
}
}
2.2 实现客户端代码:
- 前端请求时, header中统⼀添加token, 可以写在common.js中
$(document).ajaxSend(function(e, xhr, opt) {
var user_token = localStorage.getItem("user_token");// 从localStorage中拿到token值user_token
xhr.setRequestHeader("user_token_header", user_token);// 把拿到的user_token存到header中user_token_header
});
ajaxSend() ⽅法在 AJAX 请求开始时执⾏函数
- event- 包含 event 对象
- xhr- 包含 XMLHttpRequest 对象
- options- 包含 AJAX 请求中使⽤的选项
- 修改 blog_datail.html
error: function(error) {
if (error!= null && error.status == 401) {
location.href = "blog_login.html";
}
}
- 修改 blog_list.html
- 访问⻚⾯时, 添加失败处理代码
- 使⽤ location.href进⾏⻚⾯跳转.
error: function(error) {
if (error!= null && error.status == 401) {
// console.log(userToken);
alert("用户未登录,即将跳转到登录页!");
location.href = "blog_login.html";
}
}
运行程序,验证效果。
访问http://127.0.0.1:8080/blog_list.html,页面显示:
跳转到登录页面:
登录成功之后,页面跳转到博客列表页:
3. 实现显示用户信息
⽬前页面的用户信息部分是写死的。我们期望这个信息可以随着用户登录⽽发⽣改变。
- 如果当前页面是博客列表页, 则显示当前登陆用户的信息
- 如果当前页面是博客详情页, 则显示该博客的作者用户信息
注意:当前我们只是实现了显⽰用户名, 没有实现显⽰用户的头像以及文章数量等信息。
3.1 约定前后端交互接口
在博客列表页, 获取当前登陆用户的信息:
[请求]
/user/getUserInfo
[响应]
{
userId: 1,
username: test
...
}
在博客详情页, 获取当前⽂章作者的用户的信息:
[请求]
/user/getAuthorInfo?blogId=1
[响应]
{
userId: 1,
username: test
}
3.2 实现服务端代码
在 UserController添加代码:
/**
* 获取当前登录用户的信息
*/
@RequestMapping("/getUserInfo")
public UserInfo getUserInfo(HttpServletRequest request) {
// 1.获取token,从token中获取用户Id
// 2.根据用户Id,获取用户信息
String token = request.getHeader(Constant.USER_TOKEN_HEADER);
// 解析token,从token中获取用户id
Integer userId = JwtUtils.getUserIdFromToken(token);
if (userId == null || userId < 1) {
return null;
}
UserInfo userInfo = userService.queryUserById(userId);
userInfo.setPassword("");// 修改password为空
return userInfo;
}
/**
* 根据博客Id,获取作者信息
*/
@RequestMapping("/getAuthorInfo")
public UserInfo getAuthorInfo(Integer blogId) {
// 1.根据博客ID,获取作者ID
// 2.根据作者Id,获取作者信息
if (blogId == null || blogId < 1) {
return null;
}
UserInfo authorInfo = userService.getAuthorInfoByBlogId(blogId);
authorInfo.setPassword("");// 设置password为空
return authorInfo;
}
在UserService中添加代码:
public UserInfo queryUserById(Integer userId) {
return userMapper.selectById(userId);
}
public UserInfo getAuthorInfoByBlogId(Integer blogId) {
// 1.根据博客ID,获取作者ID
// 2.根据作者Id,获取作者信息
BlogInfo blogInfo = blogMapper.selectById(blogId);
if (blogInfo == null || blogInfo.getUserId() < 1) {
return null;
}
Integer userId = blogInfo.getUserId();
UserInfo userInfo = userMapper.selectById(userId);
return userInfo;
}
后端先验证接口返回是否正确:
运行程序,先登录页面,获取token:
携带该token访问http://127.0.0.1:8080/user/getUserInfo,接口返回结果为:
测试第二个接口,访问http://127.0.0.1:8080/user/getAuthorInfo?blogId=2,接口返回结果:
如果我们不想在接口返回结果中显示密码,可以把password设为空:
再次进行访问,返回结果:
3.3 实现客户端代码:
- 修改 blog_list.html
在响应回调函数中, 根据响应中的用户名, 更新界⾯的显⽰:
getUserInfo();
function getUserInfo() {
$.ajax({
type: "post",
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);
}
}
});
}
运行程序,访问 http://127.0.0.1:8080/blog_list.html,页面显示成功:
- 修改 blog_detail.html
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);
}
}
});
}
运行程序,访问http://127.0.0.1:8080/blog_detail.html?blogId=3,页面显示成功:
代码整合: 提取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);
}
}
});
}
引⼊common.js
<script src="js/common.js"></script>
blog_list.html 代码修改
var url = "/user/getUserInfo";
getUserInfo(url);
blog_detail.html 代码修改
//显示博客作者信息
var url = "/user/getAuthorInfo"+location.search;
getUserInfo(url);
4. 实现用户退出
前端直接清除掉token即可。
4.1 实现客户端代码:
<<注销>> 链接已经提前添加了onclick事件
在common.js中完善logout⽅法
// 用户注销
function logout() {
localStorage.removeItem("user_token");
location.href = "blog_login.html";
}
点击注销,页面跳转:
5. 实现发布博客
5.1 约定前后端交互接口
[请求]
/blog/add
title=标题&content=正⽂...
[响应]
{
"code": 200,
"msg": "",
"data": true
}
//true 成功
//false 失败
5.2 实现服务端代码:
修改 BlogController, 新增 publishBlog方法:
// 发布博客
@RequestMapping("/add")
public boolean publishBlog(String title, String content, HttpServletRequest request) {
// 1.校验参数
// 2.获取当前登录用户
// 3.博客发布
log.info("publishBlog, 接收参数:title: {}, content:{}",title, content);
if (!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
log.error("title or content 为空");
return false;
}
// 获取token,解析token,从token中获取用户ID
String token = request.getHeader(Constant.USER_CLAIM_ID);
Integer userId = JwtUtils.getUserIdFromToken(token);
if (userId == null || userId < 1) {
log.error("用户不存在");
return false;
}
BlogInfo blogInfo = new BlogInfo(title, content, userId);
Integer result = blogService.publishBlog(blogInfo);
if (result < 1) {
log.error("博客发布失败");
return false;
}
return true;
}
BlogService 添加对应的处理逻辑:
public Integer publishBlog(BlogInfo blogInfo) {
return blogMapper.insertBlog(blogInfo);
}
5.3 editor.md介绍
editor.md 是⼀个开源的⻚⾯ markdown 编辑器组件。
官⽹参⻅: http://editor.md.ipandao.com/
5.4 实现客户端代码:
修改 blog_edit.html:
function submit() {
$.ajax({
type: "post",
url: "/blog/add",
data: {
title: $("#title").val(),
content: $("#content").val()
},
success: function(result) {
if (result.code == 200 && result.data == true) {
// 发布成功
location.href = "blog_list.html";
}
// 失败
}
});
}
运行程序,点击写博客,写完之后进行发布:
5.5 修改详情页页面显示
此时会发现详情页会显⽰markdown的格式符号, 我们对页面进行处理。
- 修改 html 部分, 把博客正⽂的 div 标签, 改成
- 修改博客正⽂内容的显⽰
// 获取博客详情
$.ajax({
type: "get",
url: "/blog/getBlogDetail" + location.search,
success: function (result) {
if (result.code == 200 && result.data != null) {
var blog = result.data;
$(".right .content .title").text(blog.title);
$(".right .content .date").text(blog.createTime);
// $(".right .content .detail").text(blog.content);
editormd.markdownToHTML("detail", {
markdown: blog.content,
});
}
},
error: function (error) {
if (error != null && error.status == 401) {
location.href = "blog_login.html";
}
}
});
重新运行程序,刷新页面:
页面中markdown的格式符号已经没有了,但是页面的背景色需要修改,我们希望背景色不变:
修改 html 部分, 把博客正⽂的 div 标签加上style=“background-color: transparent;”
重新运行程序,刷新页面:
6. 实现删除/编辑博客
进⼊用户详情⻚时, 如果当前登录用户正是文章作者, 则在导航栏中显⽰ [编辑] [删除] 按钮, 用户点击时则进行相应处理。
需要实现两件事:
- 判定当前博客详情页中是否要显⽰[编辑] [删除] 按钮
- 实现方式:
- 后端提供一个接口,判断当前博客作者和登录用户是否相同
- 修改之前的 获取博客信息 的接⼝, 在响应中加上⼀个字段
- 实现方式:
- 实现编辑/删除逻辑(删除采用逻辑删除,所以和编辑其实为同⼀个接⼝)
6.1 约定前后端交互接口
- 判定当前博客详情页中是否要显⽰[编辑] [删除] 按钮,我们采用第二种实现方式:
[请求]
/blog/getBlogDetail?blogId=3
[响应]
{
"code": 200,
"errMsg": "",
"data": {
"id": 3,
"title": "insert第三篇博客",
"content": "insert博客内容",
"userId": 2,
"deleteFlag": 0,
"createTime": "2024-04-26 18:08",
"updateTime": "2024-04-26T10:08:58.000+00:00",
"isLoginUser": false
}
}
- 修改博客
[请求]
/blog/update
[参数]
Content-Type: application/json
{
"title": "测试修改⽂章",
"content": "在这⾥写下⼀篇博客",
"blogId": "3"
}
[响应]
{
"code": 200,
"errMsg": "",
"data": true
}
- 删除博客
[请求]
/blog/delete?blogId=1
[响应]
{
"code": 200,
"msg": "",
"data": true
}
6.2 实现服务器代码:
- 给BlogInfo类新增加一个字段isLoginUser:
- 修改 BlogController中 getBlogDeatail 接口中的逻辑
@RequestMapping("/getBlogDetail")
public BlogInfo queryBlogDetail(Integer blogId, HttpServletRequest request) {
log.info("getBlogDetail, 接收参数:blogId:"+ blogId);
BlogInfo blogInfo = blogService.queryBlogDetail(blogId);
// 1.获取登录用户
// 2.判断登录用户和博客作者是否相同
String token = request.getHeader(Constant.USER_TOKEN_HEADER);
Integer userId = JwtUtils.getUserIdFromToken(token);
if (userId != null && userId == blogInfo.getUserId()) {
blogInfo.setIsLoginUser(true);
}else {
blogInfo.setIsLoginUser(false);
}
return blogInfo;
}
注意:当BlogInfo中字段为包装类行和基本类型时,接口返回结果中变量名是不同的,这个结果是会影响到前端使用时的传参的:
涉及到前端代码:
- 修改 BlogController
增加 update/delete ⽅法, 处理修改/删除逻辑
// 修改博客
@RequestMapping("/update")
public boolean updateBlog(Integer blogId, String title, String content) {
log.info("update,接收参数:title:{},content:{}",title, content);
// 1.校验参数
// 2.根据博客Id,修改博客
if (blogId == null || !StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
log.error("博客不存在或者标题/内容为空");
return false;
}
BlogInfo blogInfo = new BlogInfo();
blogInfo.setId(blogId);
blogInfo.setTitle(title);
blogInfo.setContent(content);
Integer result = blogService.updateBlog(blogInfo);
if (result < 1) {
log.error("未修改成功");
return false;
}
return true;
}
// 删除博客
@RequestMapping("/delete")
public boolean deleteBlog(Integer blogId) {
BlogInfo blogInfo = new BlogInfo();
blogInfo.setId(blogId);
blogInfo.setDeleteFlag(1);
Integer result = blogService.deleteBlog(blogInfo);
if (result < 1) {
log.error("删除失败");
return false;
}
return true;
}
6.3 实现客户端代码:
- 判断是否显⽰[编辑] [删除]按钮
// 获取博客详情
$.ajax({
type: "get",
url: "/blog/getBlogDetail" + location.search,
success: function (result) {
...
}
// 是否显示编辑和删除按钮
if (result.data.isLoginUser) {
var html = "";
html += '<div class="operating">';
html += '<button onclick="window.location.href=\'blog_update.html'+location.search+'\'">编辑</button>';
html += '<button onclick="deleteBlog()">删除</button>';
html += '</div>';
$(".content").append(html);
}
},
...
});
运行程序,查看博客详情页中编辑和删除按钮是否显示正确:
当登录用户(zhangsan)和文章作者相同时,显示两个按钮:
当登录用户(zhangsan)和文章作者不同时,不显示:
- 编辑博客逻辑,修改blog_update.html
页面⾯加载时, 请求博客详情:
function getBlogInfo() {
// 1.页面加载时就调用,获取博客的详细信息
$.ajax({
type: "get",
url: "/blog/getBlogDetail" + location.search,
success: function (result) {
if (result.code == 200 && result.data != null) {
$("#blogId").val(result.data.id);
// 填title和content
$("#title").val(result.data.title);
// $("#content").val(result.data.content);
editor = editormd("editor", {
width: "100%",
height: "550px",
path: "blog-editormd/lib/",
onload: function () {
this.watch();
this.setMarkdown(result.data.content);
}
});
}
}
});
}
已经在getBlogInfo进⾏markdown编辑器的渲染了, 所以把以下代码删除
// 更新博客
function submit() {
$.ajax({
type: "post",
url: "/blog/update",
data: {
blogId: $("#blogId").val(),
title: $("#title").val(),
content: $("#content").val()
},
success: function(result) {
if (result.code == 200 && result.data) {
alert("博客更新成功!");
location.href = "blog_list.html";
}
}
});
}
运行程序,先测试更新博客:
更新前:
更新后:
// 删除博客
function deleteBlog() {
if (confirm("确定删除这篇博客吗?")) {
$.ajax({
type: "post",
url: "/blog/delete" + location.search,
success: function (result) {
if (result.code == 200 && result.data) {
location.href = "blog_list.html";
}
}
});
}
}
测试删除博客:
删除前博客列表页:
执行删除操作,删除后博客列表页:
数据库中这篇博客的delete_flag变为1:
未完,后续内容见下篇文章!