注意:该文档正在优化更新,更新内容更加逻辑,具体请查看《前后端分离项目入门开发》专栏
项目名称:ftest测试公开版(博客)
开发日期:2020-11-15
开发者:fnee
环境:工具/IDEAIU | jdk/1.8 | maven/4.0.0 | SpringBoot/2.3.5.RELEASE
系统:Windows/开发
创建新项目Spring Assistant,基础信息如下
选中添加的依赖spring-boot-devtools,spring-boot-starter-web,lombok,MySQL Drive,如果没有对应的依赖选项,可以先跳过,在项目创建完毕再在pom.xml中添加依赖,Maven依赖资源:https://mvnrepository.com/
可以参考官方文档:https://mybatis.plus/guide/
因为我们使用的是Mybatis-plus,所以在pom.xml中添加以下代码,引入MyBatis-Plus依赖,用来自动生成代码,提高开发效率
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
<version>2.1.3.RELEASEversion>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.4.0version>
dependency>
将application.properties配置文件改为application.yml,并添加以下代码(注意修改自己的数据库密码)
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ftest?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 这里是你的数据库密码
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
server:
port: 80
在包com.fnee.ftest
(以下称为“根包”)下创建class,命名为config.MybatisPlusConfig,并添加如下代码
package com.fnee.ftest.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@MapperScan("com.fnee.ftest.mapper")
public class MybatisPlusConfig {
//分页
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}
数据库结构如下
/*
Navicat MySQL Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80021
Source Host : localhost:3306
Source Schema : fneeblog
Target Server Type : MySQL
Target Server Version : 80021
File Encoding : 65001
Date: 15/11/2020 15:29:35
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_blog
-- ----------------------------
DROP TABLE IF EXISTS `t_blog`;
CREATE TABLE `t_blog` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`user_id` bigint(0) NOT NULL COMMENT '用户',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '标题',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '摘要',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '内容',
`created` datetime(0) NOT NULL ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`status` tinyint(0) NULL DEFAULT NULL COMMENT '状态0正常',
`type_id` bigint(0) NULL DEFAULT NULL COMMENT '分栏',
`visit` bigint(0) NULL DEFAULT 0 COMMENT '访问数量',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_type
-- ----------------------------
DROP TABLE IF EXISTS `t_type`;
CREATE TABLE `t_type` (
`id` bigint(0) NOT NULL,
`user_id` bigint(0) NULL DEFAULT NULL,
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`status` tinyint(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
`nick` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`status` int(0) NOT NULL COMMENT '状态',
`created` datetime(0) NULL DEFAULT NULL COMMENT '创建日期',
`last_login` datetime(0) NULL DEFAULT NULL COMMENT '最后登录',
PRIMARY KEY (`id`) USING BTREE,
INDEX `UK_USERNAME`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
在根包下创建代码生成类,代码如下(注意修改自己的数据库密码)
package com.fnee.ftest;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
/**
*
* 读取控制台内容
*
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("fnee"); //这里是作者名称
gc.setOpen(false);
gc.setServiceName("%sService"); //Service接口类命名规则
gc.setServiceImplName("%sServiceImpl"); //Service实现类
gc.setControllerName("%sAction"); //Controller类
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/ftest?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("你自己的数据库密码");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("com.fnee.ftest");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/"
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("t_"); //数据库名前缀,生成类时用来除去数据库名前缀
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
然后运行main方法,输入表名回车即可自动生成代码,如果出错可能是数据库配置问题
在数据库中的t_user表中添加一条id为1的数据,在UserAction中添加以下代码,然后运行FneeblogApplication,在浏览器中输入http://localhost/user/index查看是否有数据输出
package com.fnee.ftest.controller;
import com.fnee.ftest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 前端控制器
*
*
* @author fnee
* @since 2020-11-15
*/
@RestController
@RequestMapping("/user")
public class UserAction {
@Autowired
UserService userService;
@GetMapping("/index")
public Object index(){
return userService.getById(1L);
}
}
在前端请求之后,为方便数据处理,我们往往对后端返回的数据进行格式统一封装
在IDEA中打开依次打开Settings…->Plugins,搜索lombok,点击install进行安装
在根包下创建class,命名为common.lang.Result,并添加以下代码
package com.fnee.ftest.common.lang;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result implements Serializable {
private int code; //200正常,非200异常
private String msg; //返回提示信息
private Object data; //返回数据
public static Result success(int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static Result success(Object data) {
return success(200, "操作成功", data);
}
public static Result fail(int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static Result fail(String msg, Object data) {
return fail(400, msg, data);
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
}
下载地址:https://github.com/tporadowski/redis/releases
下载后解压,将redis路径添加到windows环境Path中,并将redis.windows.conf复制到用户根目录下(当cmd运行时默认是该目录,可以直接进行调用)
以后可以通过redis-server redis.windows.conf
运行redis
官方文档:https://github.com/alexxiyang/shiro-redis/tree/master/docs
在pom.xml中添加以下依赖
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redis-spring-boot-starterartifactId>
<version>3.3.1version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.4.7version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
在application.yml中添加如下代码
shiro-redis:
enables: true
redis-manager:
host: 127.0.0.1:6379
fnee:
jwt:
# 加密密匙,可以通过uuid生成
secret: dd1esdk5f6ed424e918csad7f5cbb643
# token有效时长,7天,单位秒
expire: 604800
header: Authorization
在根包下创建class,命名为shiro.AccountRealm,并添加@Component注解,继承AuthorizingRealm重写doGetAuthorizationInfo,doGetAuthenticationInfo两个方法
在config包下创建ShiroConfig类,添加如下代码,如果遇到AccountRealm accountRealm无法注入问题,可能是由于Unmapped Spring configuration files found.
报错造成的,Ctrl+Shift+Alt+S,打开Project Structure界面,选中该项目Spring,点击加号,将该项目所有内容打钩,点击OK,如果没有作用重启软件试一试,正常应该是有两处参数错误RedisSessionDAO和RedisCacheManager,可以暂时忽略
package com.fnee.ftest.shiro;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
@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;
}
}
在根包下创建util包,并添加以下工具类代码
package com.fnee.ftest.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "fnee.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();
}
/**
* 解析token
*/
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());
}
}
在shiro包下添加class,命名为JwtToken,并继承AuthenticationToken重写getPrincipal,getCredentials两个方法,最终代码如下
package com.fnee.ftest.shiro;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
在shiro包下添加class,命名为JwtFilter,并继承AuthenticatingFilter重写createToken,onAccessDenied两个方法。
createToken方法用来生成令牌:获取用户提交的token并生成令牌来验证是否登录状态。
onAccessDenied方法用来登录操作:如果用户未登录,则会执行该方法,进行逻辑判断后执行登录操作。
因为我们这个项目是前后端分离的,所以不能通过session来验证登录,所以我们使用token,在请求头中添加Authorization来保存token
package com.fnee.ftest.shiro;
import com.fnee.ftest.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
@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"); //获取前端传过来的token
if (StringUtils.isEmpty(jwt)) { //判断是否有token,也就是判断是否已登录
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; //token为空则返回true验证不通过
} else {
//校验jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
throw new ExpiredCredentialsException("token已失效,请重新登录");
}
//执行登录
return executeLogin(servletRequest, servletResponse);
}
}
}
为了使登录失败后返回的格式符合我们的要求,所以重写onLoginFailure方法
/**
* 重写onLoginFailure,将返回的异常格式改为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;
}
在shiro包中创建class,命名为AccountProfile,并添加如下代码
package com.fnee.ftest.shiro;
import lombok.Data;
import java.io.Serializable;
@Data
public class AccountProfile implements Serializable {
//授权用户信息
private Long id;
private String nick;
private String username;
private String avatar;
private String email;
}
在AccountRealm中的doGetAuthenticationInfo方法中添加逻辑代码,最终代码:
package com.fnee.ftest.shiro;
import cn.hutool.core.bean.BeanUtil;
import com.fnee.ftest.entity.User;
import com.fnee.ftest.service.UserService;
import com.fnee.ftest.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//当前登录用户授权
JwtToken jwtToken = (JwtToken) authenticationToken;
//通过token获取用户id
String userId = jwtUtils.getClaimByToken((String) jwtToken.getCredentials()).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());
}
}
在ShiroConfig中添加以下两个方法,并手动导入import org.apache.shiro.mgt.SecurityManager;
包,Filter导入javax.servlet中的Filter,并注入JwtFilter
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilterFactoryBean.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
在common包下创建exception包,并创建class,命名为GlobalExceptionHandler,并添加如下代码
package com.fnee.ftest.common.exception;
import com.fnee.ftest.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(value = ShiroException.class)
public Result handler(ShiroException e) {
log.error("运行时异常:-----------------{}", e);
return Result.fail(401,e.getMessage(),null);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) {
log.error("实体校验异常:-----------------{}", e);
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
return Result.fail(objectError.getDefaultMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:-----------------{}", e);
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:-----------------{}", e);
return Result.fail(e.getMessage());
}
}
因为整合了redis,所以启动项目时也要启动redis
spring-devtools.properties
文件,并添加restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
内容按照测试一再次测试,正常的话我们能访问到数据,再给index方法添加@RequiresAuthentication
注解,再次访问结果如下
后端通过Hibernate Validator来校验数据。在执行Controller之前,首先会校验数据格式,校验不通过则不会执行Controller中对应的方法,以下是常用注解
注释 | 验证类型 | 验证规则 |
---|---|---|
@AssertFalse | Boolean,boolean | 验证注解的元素值是false |
@AssertTrue | Boolean,boolean | 验证注解的元素值是true |
@NotNull | 任意类型 | 验证注解的元素值不是null |
@Null | 任意类型 | 验证注解的元素值是null |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) | 和@Min要求一样 | 验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) | CharSequence子类型(如String) | 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
如对User类进行数据校验,可以这样写(注意包的引入)。其中@JsonFormat是用来规定格式的,当数据从后端响应给前端时,会根据规定的格式进行转化
package com.fnee.ftest.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_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 = "昵称不能为空")
private String nick;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
private String password;
private Integer status;
@JsonFormat(pattern = "yyy-MM-dd HH:mm:ss")
private LocalDateTime created;
private LocalDateTime lastLogin;
}
在UserAction中添加以下方法,@Validated注解用来校验数据格式
@PostMapping("/save")
public Result save(@Validated @RequestBody User user){
return Result.success(user);
}
打开Postman,输入http://localhost/user/save
,选择Post并添加Body如下
点击Send,返回数据如下(因为我们在全局异常处理中添加了用来捕获实体校验异常的方法,所以返回格式如下),其他测试请自行尝试
为了解决前后端分离出现的跨域问题,我们添加Spring配置来解决跨域问题
package com.fnee.ftest.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
以为Shiro过滤器在Controller之前进行调用,所以在JwtFilter中添加以下代码来解决跨域问题
/**
* 解决跨域
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) 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(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
在common包下创建class,命名为dto.LoginDto和dto.LoginUser
package com.fnee.ftest.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
@Data
public class LoginDto implements Serializable {
@NotBlank(message = "昵称不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
package com.fnee.ftest.common.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class LoginUser implements Serializable {
private Long id;
private String username;
private String nick;
private String avatar;
private String email;
@JsonFormat(pattern = "yyy-MM-dd HH:mm:ss")
private LocalDateTime created;
private LocalDateTime lastLogin;
private Long visit;
}
将UserAction恢复初始,并将所有Action去掉@RequestMapping,注入UserService
@Autowired
UserService userService;
@PostMapping("/login")
public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {
//通过前端传过来的用户名获取数据库中对应的User信息
User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
//判断是否有该用户信息,全局异常处理类中含有捕获Assert异常方法
Assert.notNull(user, "用户名不存在");
//密码对比
if (!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
return Result.fail("密码不正确");
}
//根据用户id生成token
String jwt = jwtUtils.generateToken(user.getId());
response.setHeader("Authorization", jwt); //将token保存到header的Authorization属性中
//将Authorization属性设置为可公开属性,不然前端拿不到
response.setHeader("Access-Control-Expose-Headers", "Authorization");
LoginUser loginUser = new LoginUser();
//复制user属性给loginUser,忽略password属性(这里是为了避免用户信息泄露)
BeanUtils.copyProperties(user,loginUser,"password");
return Result.success(loginUser); //返回登录后的用户信息
}
@RequiresAuthentication
@GetMapping("/logout")
public Result logout() {
SecurityUtils.getSubject().logout(); //将当前用户注销
return Result.success(null);
}
在BlogAction中添加以下代码
@Autowired
BlogService blogService;
@GetMapping("/blogs")
public Result list(@RequestParam(defaultValue = "1") Integer currentPage) {
//创建分页实例,currentPage是第几页,10代表一页几条数据
Page page = new Page(currentPage, 10);
//根据page获取博客数据
IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));
return Result.success(pageData);
}
@GetMapping("/blogs/{type}")
public Result listWithType(@PathVariable(name = "type") Long type, @RequestParam(defaultValue = "1") Integer currentPage) {
Page page = new Page(currentPage, 10);
//在获取博客数据时,添加eq根据属性来查询数据
IPage pageData = blogService.page(page, new QueryWrapper<Blog>().eq("type_id", type).orderByDesc("created"));
return Result.success(pageData);
}
@GetMapping("/blogs/hot")
public Result hot() {
Page page = new Page(1, 10); //只获取第一页,10条信息
//根据阅读量排序获取数据
Page pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("visit"));
return Result.success(pageData);
}
@GetMapping("/blog/{id}")
public Result detail(@PathVariable(name = "id") Long id) {
Blog blog = blogService.getById(id); //通过id获取博客信息
Assert.notNull(blog, "该博客已被删除"); //判断博客信息是否为空
Long read = blog.getVisit(); //获取阅读量
blog.setVisit(++read); //阅读量加1并赋值
blogService.updateById(blog); //更新博客数据
return Result.success(blog);
}
@RequiresAuthentication
@PostMapping("/blog/edit")
public Result list(@Validated @RequestBody Blog blog) {
Blog temp = null;
//根据前端传过来的数据是否有id判断添加或是修改
if (blog.getId() != null) {
//编辑文章
temp = blogService.getById(blog.getId());
//只能编辑自己的文章,ShiroUtil.getProfile().getId()获取当前登录用户id
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);
}
BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
blogService.saveOrUpdate(temp);
return Result.success(null);
}
@RequiresAuthentication
@GetMapping("/blog/visits")
public Result blogVisit() {
Long nums = 0L;
List<Blog> list = blogService.list(new QueryWrapper<Blog>().eq("user_id", ShiroUtil.getProfile().getId()));
for (int i = 0, len = list.size(); i < len; i++) {
Blog blog = list.get(i);
nums += blog.getVisit();
}
return Result.success(nums);
}
在TypeAction中添加以下代码
@Autowired
TypeService typeService;
@GetMapping("/type")
public Result type() {
return Result.success(typeService.list());
}
@RequiresAuthentication
@GetMapping("/type/nums")
public Result typeNum() {
return Result.success(typeService.count(new QueryWrapper<Type>().eq("user_id", ShiroUtil.getProfile().getId())));
}
下载地址:https://download.csdn.net/download/jl15988/13183885