前言
1、項目背景
學習完SpringBoot、SSM框架、VUE框架,一致苦于沒有項目去結合起來。通過這個博客練習項目,鍛煉自己,鞏固學習。此項目并非原創,是在b站中搜索到的,來自`MarkerHub UP主`,[【實戰】基于SpringBoot+Vue開發的前后端分離博客項目完整教學](https://www.bilibili.com/video/BV1PQ4y1P7hZ?p=14),學完此項目,后續可能夠會增加自己的一些內容。
Java后端接口開發
1、前言
技術棧:
- SpringBoot 2.4.3
- mybatis plus
- shiro
- lombok
- redis
- hibernate validatior
- jwt
2、項目搭建
開發工具及環境:
- idea
- mysql
- jdk 8
- maven3.3.9
idea創建工程,工程名ergoublog-api,選上 熱加載工具,Lombok,web,mysql驅動,其他的需要的后續在加
新建立好的項目結構如下,springboot版本2.4.3
pom的jar包引入如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- devtools:項目的熱加載重啟插件
- lombok:簡化代碼的工具
3、整合mybatis plus
整合mybatis plus 目的讓我們快速進行單表的CRUD操作,詳情見官網:https://mp.baomidou.com/guide/install.html#release
1、導入jar
pom中導入mybatis plus的jar包,因為后面會涉及到代碼生成,所以我們還需要導入頁面模板引擎,這里我們使用 Freemarker
<!--freemarker模板-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--mp代碼生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.2</version>
</dependency>
2、再application.yml文件中配置mybatis plus
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3308/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
上面除了配置數據庫的信息,還配置了myabtis plus的mapper的xml文件的掃描路徑,這一步不要忘記了。
3、開啟mapper接口掃描,添加分頁插件
- com.sen.config.MybatisPlusConfig
@Configuration
@EnableTransactionManagement
@MapperScan("com.sen.mapper")
public class MybatisPlusConfig {
// 舊版
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 設置請求的頁面大于最大頁后操作, true調回到首頁,false 繼續請求 默認false
// paginationInterceptor.setOverflow(false);
// 設置最大單頁限制數量,默認 500 條,-1 不受限制
// paginationInterceptor.setLimit(500);
// 開啟 count 的 join 優化,只針對部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
4、數據庫建表
CREATE DATABASE `vueblog`;
USE `vueblog`;
CREATE TABLE `m_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(64) DEFAULT NULL,
`avatar` VARCHAR(255) DEFAULT NULL,
`email` VARCHAR(64) DEFAULT NULL,
`password` VARCHAR(64) DEFAULT NULL,
`status` INT(5) NOT NULL,
`created` DATETIME DEFAULT NULL,
`last_login` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `m_blog` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`content` LONGTEXT,
`created` DATETIME NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`status` TINYINT(4) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;
注:
如果【Mysql 建表時報錯 invalid ON UPDATE clause for 'create_date' column】
這個錯誤是由MySQL版本問題導致的,建議換mysql8.0 ,官方下載地址:https://downloads.mysql.com/archives/community/
但是我想mysql5.5和mysql8.0共存,參考文章:https://blog.csdn.net/zemuerqi/article/details/107329378
參考文章https://juejin.cn/post/6844903823966732302#heading-3
4、代碼生成
-
com.sen.CodeGenerator
去官網自己拿:https://mp.baomidou.com/guide/generator.html#%E6%B7%BB%E5%8A%A0%E4%BE%9D%E8%B5%96
也可以去我的gitee取
運行CodeGenerator的main方法
得到
簡潔!方便!經過上面的步驟,基本上我們已經把mybatis plus框架集成到項目中了。
在UserController中寫個測試:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
public Object test(@PathVariable("id") Long id){
return userService.getById(id);
}
}
訪問:http://localhost:8080/user/1 獲得結果如下,整合成功!
4、統一結果封裝
這里我們用一個Result的類,這個用于我們的異步統一返回的結果封裝。一般來說,結果里面有幾個要素必要的
- 是否成功,可用code表示,(如300表示成功,400表示異常)
- 結果消息
- 結果數據
所以可得到封裝如下
-
com.sen.common.lang.Result
@Data public class Result implements Serializable { private String code; private String msg; private Object data; public static Result succ(Object data) { return succ("操作成功",data); } public static Result succ(String mess, Object data) { Result m = new Result(); m.setCode("0"); m.setData(data); m.setMsg(mess); return m; } public static Result succ(String mess) { return fail(mess,null); } public static Result fail(String mess, Object data) { Result m = new Result(); m.setCode("-1"); m.setData(null); m.setMsg(mess); return m; } }
5、整合shiro+jwt,并會話共享
考慮到后面可能需要做集群、負載均衡等,所以就需要會話共享,而shiro的緩存和會話信息,我們一般考慮使用redis來存儲這些數據,所以,我們不僅僅需要整合shiro,配置簡單,這里也推薦大家使用
因為我們需要做的是前后端分離項目的骨架,所以一般我們會采用token或jwt作為跨域身份驗證解決方案。所以整合shiro的過程中,我們需要引入jwt的身份驗證
登錄邏輯:
用戶訪問api:
參考文檔:https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter
1、導入shiro-redis的starter的jar包,還有jwt的工具包,以及為了簡化開發,我引入hutool(https://hutool.cn/docs/#/)
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<!-- hutool工具類-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、編寫配置
ShiroConfig
-
com.sen.config.ShiroConfig
//shiro啟用注解攔截器 @Configuration public class ShiroConfig { @Autowired JwtFilter jwtFilter; @Bean public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); // inject redisSessionDAO sessionManager.setSessionDAO(redisSessionDAO); // other stuff... return sessionManager; } @Bean public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm); //inject sessionManager securityManager.setSessionManager(sessionManager); // inject redisCacheManager securityManager.setCacheManager(redisCacheManager); // other stuff... return securityManager; } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); Map<String, String> filterMap = new LinkedHashMap<>(); //所有鏈接經過jwt filterMap.put("/**", "jwt"); // 主要通過注解方式校驗權限 chainDefinition.addPathDefinitions(filterMap); return chainDefinition; } @Bean("shiroFilterFactoryBean") public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); Map<String, Filter> filters = new HashMap<>(); filters.put("jwt", jwtFilter); shiroFilter.setFilters(filters); Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap(); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } }
上面ShiroConfig,我們主要做了幾件事情:
- 引入RedisSessionDAO和RedisCacheManager,為了解決shiro的權限數據和會話信息能保存到redis中,實現會話共享。
- 重寫了SessionManager和DefaultWebSecurityManager,同時在DefaultWebSecurityManager中為了關閉shiro自帶的session方式,我們需要設置為false,這樣用戶就不再能通過session方式登錄shiro。后面將采用jwt憑證登錄。
- 在ShiroFilterChainDefinition中,我們不再通過編碼形式攔截Controller訪問路徑,而是所有的路由都需要經過JwtFilter這個過濾器,然后判斷請求頭中是否含有jwt的信息,有就登錄,沒有就跳過。跳過之后,有Controller中的shiro注解進行再次攔截,比如@RequiresAuthentication,這樣控制權限訪問。
那么,接下來,我們聊聊ShiroConfig中出現的AccountRealm,還有JwtFilter。
AccountRealm
AccountRealm是shiro進行登錄或者權限校驗的邏輯所在,算是核心了,我們需要重寫3個方法,分別是
- supports:為了讓realm支持jwt的憑證校驗
- doGetAuthorizationInfo:權限校驗
- doGetAuthenticationInfo:登錄認證校驗
我們先來總體看看AccountRealm的代碼,然后逐個分析:
-
com.sen.shiro
@Component public class AccountRealm extends AuthorizingRealm { @Autowired JwtUtils jwtUtils; @Autowired UserService userService; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } //獲取權限信息,封裝返回用戶信息 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //獲取身份驗證 獲取token,校驗成功返回信息 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { JwtToken jwtToken = (JwtToken) token; String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject(); User user = userService.getById(Long.valueOf(userId)); if (user == null){ throw new UnknownAccountException("賬號不存在!"); } if (user.getStatus() == -1) { throw new LockedAccountException("賬戶已被鎖定!"); } AccountProfile profile = new AccountProfile(); BeanUtil.copyProperties(user,profile); return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName()); } }
其實主要就是doGetAuthenticationInfo登錄認證這個方法,可以看到我們通過jwt獲取到用戶信息,判斷用戶的狀態,最后異常就拋出對應的異常信息,否者封裝成SimpleAuthenticationInfo返回給shiro。 接下來我們逐步分析里面出現的新類:
1、shiro默認supports的是UsernamePasswordToken,而我們現在采用了jwt的方式,所以這里我們自定義一個JwtToken,來完成shiro的supports方法。
JwtToken
- com.sen.shiro
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt) {
this.token = jwt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2、JwtUtils是個生成和校驗jwt的工具類,其中有些jwt相關的密鑰信息是從項目配置文件中配置的:
JwtUtils
- com.sen.util
/**
* jwt工具類
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "sen.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//過期時間
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("validate is token error ", e);
return null;
}
}
/**
* token是否過期
* @return true:過期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
3、而在AccountRealm我們還用到了AccountProfile,這是為了登錄成功之后返回的一個用戶信息的載體,
AccountProfile
- com.sen.shiro
//封裝用戶信息
@Data
public class AccountProfile implements Serializable {
private Long id;
private String username;
private String avatar;
private String email;
}
第三步,ok,基本的校驗的路線完成之后,我們需要少量的基本信息配置:
application.yml
#shiro Config
shiro-redis:
enabled : true
redis-manager:
host:127.0.0.1:6379
#jwt Config
sen:
jwt:
# 加密秘鑰
secret: f4e2e52034348f86b67cde581c0f9eb5
# token有效時長,7天,單位秒
expire: 604800
header: Authorization
spring-devtools.properties
第四步:另外,如果你項目有使用spring-boot-devtools,需要添加一個配置文件,在resources目錄下新建文件夾META-INF,然后新建文件spring-devtools.properties,這樣熱重啟時候才不會報錯。
- resources/META-INF/
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
第五步:定義jwt的過濾器JwtFilter。
JwtFilter
這個過濾器是我們的重點,這里我們繼承的是Shiro內置的AuthenticatingFilter,一個可以內置了可以自動登錄方法的的過濾器,有些同學繼承BasicHttpAuthenticationFilter也是可以的。
我們需要重寫幾個方法:
- createToken:實現登錄,我們需要生成我們自定義支持的JwtToken
- onAccessDenied:攔截校驗,當頭部沒有Authorization時候,我們直接通過,不需要自動登錄;當帶有的時候,首先我們校驗jwt的有效性,沒問題我們就直接執行executeLogin方法實現自動登錄
- onLoginFailure:登錄異常時候進入的方法,我們直接把異常信息封裝然后拋出
- preHandle:攔截器的前置攔截,因為我們是前后端分析項目,項目中除了需要跨域全局配置之外,我們再攔截器中也需要提供跨域支持。這樣,攔截器才不會在進入Controller之前就被限制了。
下面我們看看總體的代碼:
- com.sen.shiro
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if(StringUtils.isEmpty(jwt)){
//用戶沒帶jwt,返回空
return null;
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if(StringUtils.isEmpty(jwt)){
return true;
} else {
//校驗jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())){
throw new ExpiredCredentialsException("token已失效,請重新登錄");
}
//執行登錄
return executeLogin(servletRequest,servletResponse);
}
}
//處理登錄失敗,封裝成Result類型,供前端使用
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = Result.fail(throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
httpServletResponse.getWriter().print(json);
} catch (IOException ioException) {
}
return false;
}
//跨域處理(cv
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時會首先發送一個OPTIONS請求,這里我們給OPTIONS請求直接返回正常狀態
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
那么到這里,我們的shiro就已經完成整合進來了,并且使用了jwt進行身份校驗。
6、異常處理
有時候不可避免服務器報錯的情況,如果不配置異常處理機制,就會默認返回tomcat或者nginx的5XX頁面,對普通用戶來說,不太友好,用戶也不懂什么情況。這時候需要我們程序員設計返回一個友好簡單的格式給前端。
處理辦法如下:通過使用@ControllerAdvice來進行統一異常處理,@ExceptionHandler(value = RuntimeException.class)來指定捕獲的Exception各個類型異常 ,這個異常的處理,是全局的,所有類似的異常,都會跑到這個地方處理。
- com.sen.common.exception
定義全局異常處理,@ControllerAdvice表示定義全局控制器異常處理,@ExceptionHandler表示針對性異常處理,可對每種異常針對性處理。
//捕獲異步異常,
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHander {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result hander(RuntimeException e){
log.error("運行時異常:------------>",e);
return Result.fail(e.getMessage());
}
// 捕捉shiro的異常
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(value = ShiroException.class)
public Result hander(ShiroException e){
log.error("運行時異常:------------>",e);
return Result.fail(401,e.getMessage(),null);
}
}
上面我們捕捉了幾個異常:
- ShiroException:shiro拋出的異常,比如沒有權限,用戶登錄異常
- IllegalArgumentException:處理Assert的異常
- MethodArgumentNotValidException:處理實體校驗的異常
- RuntimeException:捕捉其他異常
7、實體校驗
當我們表單數據提交的時候,前端的校驗我們可以使用一些類似于jQuery Validate等js插件實現,而后端我們可以使用Hibernate validatior來做校驗。
我們使用springboot框架作為基礎,那么就已經自動集成了Hibernate validatior。
那么用起來啥樣子的呢?
第一步:首先在實體的屬性上添加對應的校驗規則,比如:
@TableName("m_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@NotBlank(message = "昵稱不能為空")
private String username;
private String avatar;
@NotBlank(message = "郵箱格式不能為空")
@Email(message = "郵箱格式不正確")
private String email;
...
}
第二步 :這里我們使用@Validated注解方式,如果實體不符合要求,系統會拋出異常,那么我們的異常處理中就捕獲到MethodArgumentNotValidException。
- com.sen.controller
@RestController
@RequestMapping("/user")
public class UserController {
//測試實體校驗
@PostMapping("/save")
public Result save(@Validated @RequestBody User user){
return Result.succ(user);
}
}
- com.sen.common.exception
//捕獲異步異常,
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHander {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result hander(MethodArgumentNotValidException e){
log.error("實體校驗異常:------------{}",e);
//簡要輸出錯誤信息
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
return Result.fail(401,objectError.getDefaultMessage(),null);
}
...
}
注:springboot在2.3版本之后不在主動提供實體校驗方法,需要手動添加依賴包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
測試:
8、跨域問題
因為是前后端分析,所以跨域問題是避免不了的,我們直接在后臺進行全局跨域處理:
- com.sen.config.CorsConfig
/**
* 解決跨域問題(cv)
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
因為用戶請求在進入controller之前是會先經過我們的jwtFilter的,所以jwtFilter在做事情之前,我們需要進行跨域處理
//跨域處理(cv
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時會首先發送一個OPTIONS請求,這里我們給OPTIONS請求直接返回正常狀態
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
到此,我們springboot的腳手架 基本上已經搭建完成,接下來進行接口開發,這次系統開發的接口比較簡單,所以我就不集成swagger2,也比較簡單
9、登錄接口開發
登錄的邏輯其實很簡答,只需要接受賬號密碼,然后把用戶的id生成jwt,返回給前端,為了后續的jwt的延期,所以我們把jwt放在header上。具體代碼如下:
- com.sen.common.dto
封裝用戶信息
@Data
public class LoginDto implements Serializable {
@NotBlank(message = "昵稱不能為空")
private String username;
@NotBlank(message = "密碼不能為空")
private String password;
}
-
com.sen.common.exception.GlobalExceptionHander
添加斷言異常
//斷言異常
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result hander(IllegalArgumentException e){
log.error("Assert異常:------------{}",e);
return Result.fail(e.getMessage());
}
- com.sen.controller.AccountController
@RestController
public class AccountController {
@Autowired
UserService userService;
@Autowired
JwtUtils jwtUtils;
@RequestMapping("/login")
public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
Assert.notNull(user,"用戶不存在");//斷言攔截
//判斷賬號密碼是否錯誤 因為是md5加密所以這里md5判斷
if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
//密碼不同則拋出異常
return Result.fail("密碼不正確");
}
String jwt = jwtUtils.generateToken(user.getId());
//將token 放在我們的header里面
response.setHeader("Authorization",jwt);
response.setHeader("Access-control-Expose-Headers","Authorization");
return Result.succ(MapUtil.builder()
.put("id",user.getId())
.put("username",user.getUsername())
.put("avatar",user.getAvatar())
.put("email",user.getEmail()).map()
);
}
//需要認證權限才能推出登錄
@RequiresAuthentication
@GetMapping("/logout")
public Result logout(){
SecurityUtils.getSubject().logout();
return Result.succ(null);
}
}
測試
10、博客接口開發
我們的骨架已經完成,接下來,我們就可以添加我們的業務接口了,下面我以一個簡單的博客列表、博客詳情頁為例子開發:
- com.sen.controller.BlogController
@RestController
public class BlogController {
@Autowired
BlogService blogService;
//列表頁
@GetMapping("/blogs")
public Result list(@RequestParam(defaultValue = "1") Integer currentPage){
//分頁
Page page = new Page(currentPage,5);
IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));
return Result.succ(pageData);
}
//詳情頁
@GetMapping("/blog/{id}")
public Result detail(@PathVariable(name = "id") Long id){
Blog blog = blogService.getById(id);
//如果沒有查詢到,斷言告訴前端,記錄查詢為空
Assert.notNull(blog,"該博客已被刪除");
return Result.succ(blog);
}
//編輯頁,編輯和添加博客放在一起,需要權限才能編輯
@RequiresAuthentication
@PostMapping("/blog/edit")
public Result list(@Validated @RequestBody Blog blog){
Blog temp = null;
//編輯博客
if (blog.getId() != null){
temp = blogService.getById(blog.getId());
//只能編輯自己的文章
Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(),"你沒有權限編輯");
} else {
temp = new Blog();
temp.setUserId(ShiroUtil.getProfile().getId());
temp.setCreated(LocalDateTime.now());
temp.setStatus(0);
}
BeanUtils.copyProperties(blog,temp,"id","userId","created","status");
blogService.saveOrUpdate(temp);
return Result.succ(null);
}
//刪除頁,需要權限才能刪除
@RequiresAuthentication
@GetMapping("/blog/delete/{id}")
public Result delete(@PathVariable(name = "id") Long id){
Blog blog = blogService.getById(id);
//如果沒有查詢到,斷言告訴前端,記錄查詢為空
Assert.notNull(blog,"該博客已被刪除");
blogService.removeById(id);
return Result.succ(null);
}
}
注意@RequiresAuthentication說明需要登錄之后才能訪問的接口,其他需要權限的接口可以添加shiro的相關注解。 接口比較簡單,我們就不多說了,基本增刪改查而已。注意的是edit方法是需要登錄才能操作的受限資源。
- com.sen.entity.Blog
實體校驗
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("m_blog")
public class Blog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long userId;
@NotBlank(message = "博客標題不能為空")
private String title;
@NotBlank(message = "博客摘要不能為空")
private String description;
@NotBlank(message = "博客內容不能為空")
private String content;
...
}
其中用到自定義的工具類ShiroUtil
- com.sen.util.ShiroUtil
public class ShiroUtil {
public static AccountProfile getProfile(){
return (AccountProfile)SecurityUtils.getSubject().getPrincipal();
}
}
測試:
登錄,取請求頭的jwt
詳情頁
檢查實體校驗
檢測權限,@RequiresAuthentication起作用
測試編輯
這里可能是long封裝問題
Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(),"你沒有權限編輯");
查看數據庫更改成功
刪除
11、小結
后端的腳手架,接口已經初步搭建完成,也測試成功,后面就開始做前端頁面開發!