blog前后端分離項目01:Java后端接口開發

前言

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、代碼生成

得到

簡潔!方便!經過上面的步驟,基本上我們已經把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,我們主要做了幾件事情:

  1. 引入RedisSessionDAO和RedisCacheManager,為了解決shiro的權限數據和會話信息能保存到redis中,實現會話共享。
  2. 重寫了SessionManager和DefaultWebSecurityManager,同時在DefaultWebSecurityManager中為了關閉shiro自帶的session方式,我們需要設置為false,這樣用戶就不再能通過session方式登錄shiro。后面將采用jwt憑證登錄。
  3. 在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也是可以的。

我們需要重寫幾個方法:

  1. createToken:實現登錄,我們需要生成我們自定義支持的JwtToken
  2. onAccessDenied:攔截校驗,當頭部沒有Authorization時候,我們直接通過,不需要自動登錄;當帶有的時候,首先我們校驗jwt的有效性,沒問題我們就直接執行executeLogin方法實現自動登錄
  3. onLoginFailure:登錄異常時候進入的方法,我們直接把異常信息封裝然后拋出
  4. 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、小結

后端的腳手架,接口已經初步搭建完成,也測試成功,后面就開始做前端頁面開發!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容