Part01-集成MP完成项目初始化
blog-tiny
│ pom.xml
│
└─src
└─main
├─java
│ └─org
│ └─org.myslayers
│ │ Application.java
│ │ CodeGenerator.java
│ │
│ ├─common
│ │ └─lang
│ │ Result.java
│ │
│ ├─config
│ │ MyBatisPlusConfig.java
│ │
│ ├─controller
│ │ BaseController.java
│ │ PostController.java
│ │ UserController.java
│ │
│ ├─entity
│ │ BaseEntity.java
│ │ Post.java
│ │ User.java
│ │
│ ├─mapper
│ │ PostMapper.java
│ │ UserMapper.java
│ │
│ └─service
│ │ PostService.java
│ │ UserService.java
│ │
│ └─impl
│ PostServiceImpl.java
│ UserServiceImpl.java
│
└─resources
│ application-win.yml
│ application.yml
│ spy.properties
│
├─mapper
│ PostMapper.xml
│ UserMapper.xml
│
├─static
└─templates
1 MP 环境
pom.xml
:项目依赖
<dependencies>
<!--mp、druid、mysql、mp-generator(MyBatis-Plus 从 3.0.3后移除了代码生成器与模板引擎的默认依赖)、MP支持的SQL分析器、MP代码生成使用 freemarker 模板引擎-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>druid-spring-boot-starter</artifactId>-->
<!-- <version>1.1.10</version>-->
<!-- </dependency>-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.8.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
application.yml
:配置文件,【识别 Mapper 层】
spring:
datasource:
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://127.0.0.1:3306/xblog_tiny?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://127.0.0.1:3306/xblog_tiny?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
server:
port: 8080
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
spy.properties
:配置文件,【p6spy 组件对应的 spy.properties 配置】
#3.2.1以下使用或者不配置
module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
2 MP 代码生成器
- CodeGenerator.java:代码生成器,【配置 MySQL 相关信息,比如表名、用户名、密码】
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
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.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
final String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("org/myslayers");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(
"jdbc:mysql://localhost:3306/xblog_tiny?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("org.org.myslayers");
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.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.setSuperEntityClass("org.org.myslayers.entity.BaseEntity");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 你自己的父类控制器,没有就不用设置!
strategy.setSuperControllerClass("org.org.myslayers.controller.BaseController");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setSuperEntityColumns("id", "created", "modified");
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("m_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
3 MP 分页
MyBatisPlusConfig.java
:配置类
/**
* MP配置类
*/
@Configuration
@EnableTransactionManagement
@MapperScan("org.org.myslayers.mapper")
public class MyBatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
4 前后端分离,统一封装返回结果
Result.java
:实体类,【前后端分离,统一封装返回结果】
/**
* 统一封装返回结果
*/
@Data
public class Result implements Serializable {
// 操作状态:200代表成功,非200为失败/异常
private int code;
// 携带msg
private String msg;
// 携带data
private Object data;
public static Result success(int code, String msg, Object data) {
Result result = new Result();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static Result success(String msg) {
return Result.success(200, msg, null);
}
public static Result success(String msg, Object data) {
return Result.success(200, msg, data);
}
public static Result fail(int code, String msg, Object data) {
Result result = new Result();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static Result fail(String msg) {
return fail(500, msg, null);
}
public static Result fail(String msg, Object data) {
return fail(500, msg, data);
}
}
Part02-集成Shiro-Redis-Jwt实现会话共享(一)
blog-tiny
│ pom.xml
│
└─src
└─main
├─java
│ └─org
│ └─org.myslayers
│ ├─config
│ │ ShiroConfig.java
│ │
│ ├─shiro
│ │ AccountProfile.java
│ │ AccountRealm.java
│ │ JwtFilter.java
│ │ JwtToken.java
│ │
│ └─utils
│ JwtUtils.java
│
└─resources
│ application-win.yml
│ application.yml
│
└─META-INF
spring-devtools.properties
5 集成 Shiro-Redis、Jwt 环境
pom.xml
:项目依赖,【shiro-redis、jwt 实现会话共享、身份验证】
<dependencies>
<!--shiro-redis、jwt 实现会话共享、身份验证-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
6 编写 ShiroConfig.java 配置文件
ShiroConfig.java
:配置类,【安全管理器、过滤器链、过滤器工厂】
/**
* 配置类:安全管理器、过滤器链、过滤器工厂
*/
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
/**
* 1.安全管理器:根据 “https://github.com/alexxiyang/shiro-redis/tree/master/docs” 说明,将 【自定义Realm】、【自定义的session会话管理器】、【自定义的redis缓存管理器】 注入 DefaultWebSecurityManager,并关闭shiro自带的session
*
* 具体内容如下:
* - 引入 RedisSessionDAO 和 RedisCacheManager,为了解决 shiro 的权限数据和会话信息能保存到 redis 中,实现会话共享
* - 重写 SessionManager 和 DefaultWebSecurityManager,同时在 DefaultWebSecurityManager 中为了关闭 shiro 自带的 session 方式,我们需要设置为 false,这样用户就不再能通过 session 方式登录 shiro,而后将采用 jwt 凭证登录
*/
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
// 将 自定义Realm 注册到安全管理器中
securityManager.setRealm(accountRealm);
// 将 自定义的session会话管理器 注册到安全管理器中
securityManager.setSessionManager(sessionManager);
// 将 自定义的redis缓存管理器 注册到安全管理器中
securityManager.setCacheManager(redisCacheManager);
// 关闭 shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 2.过滤器链
*
* 具体内容如下:不再通过编码形式拦截 Controller 访问路径,而将全部路由经过 JwtFilter 处理
* - 如果 JwtFilter 在判断请求头时,如果存在 jwt 信息,校验 jwt 的有效性,如果有效,则直接执行 executeLogin 方法实现自动登录
* - 如果 JwtFilter 在判断请求头时,如果没有 jwt 信息,则跳过;跳过之后,有 Controller 中的 Shiro 注解【@RequiresAuthentication】 来控制权限访问
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt"); // 将全部路由交给 JwtFilter 过滤器进行处理
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
/**
* 3.过滤器工厂
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
// shiroFilter 设置 自定义安全管理器(securityManager)
shiroFilter.setSecurityManager(securityManager);
// shiroFilter 设置 自定义jwtFilter
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
// shiroFilter 设置 自定义过滤器链(chainDefinition)
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
7 编写 JwtFilter.java 配置文件
- 采用 Jwt 作为跨域身份验证解决方案,原理如下:
JwtFilter.java
:Filter,【继承 Shiro 内置的 AuthenticatingFilter】
/**
* Filter:继承 Shiro 内置的 AuthenticatingFilter
*/
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
/**
* createToken:实现登录,【从 request 的 header 中获取 自定义的jwt(Authorization),然后生成 JwtToken 信息并返回】
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return null;
}
return new JwtToken(jwt);
}
/**
* onAccessDenied:拦截校验,
* - 如果 JwtFilter 在判断请求头时,如果存在 jwt 信息,校验 jwt 的有效性,如果有效,则直接执行 executeLogin 方法实现自动登录
* - 如果 JwtFilter 在判断请求头时,如果没有 jwt 信息,则跳过;跳过之后,有 Controller 中的 Shiro 注解【@RequiresAuthentication】 来控制权限访问
*/
@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);
}
}
/**
* onLoginFailure:登录异常时候进入的方法,【前后端项目,使用JSON格式的数据,将异常信息封装然后抛出】
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = Result.fail(throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.getWriter().print(json);
} catch (IOException ioException) {
}
return false;
}
/**
* preHandle:拦截器的前置拦截,【前后端项目,除了需要跨域全局配置之外,拦截器中也需要提供跨域支持。这样,拦截器才不会在进入 Controller 之前就被限制了】
*/
@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);
}
}
JwtUtils.java
:生成和校验 jwt 的工具类:【来自 application.yml 配置文件】
/**
* 生成和校验 jwt 的工具类:【来自 application.yml 配置文件】
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "org.myslayers.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();
}
/**
* 获取 jwt 信息
*/
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 是否过期,true代表过期;false代表有效
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
application.yml
:配置文件,用于 JwtUtils.java 的 属性注入,【开启 shiro-redis 会话管理、向 JwtUtils.java 类中的属性注入值】
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
password: org.myslayers
org.myslayers:
jwt:
# 加密秘钥
secret: f4e2e52034348f86b67cde581c0f9eb5
# token有效时长,7天,单位秒
expire: 604800
# header
header: authorization
spring-devtools.properties
:配置文件,【项目使用 spring-boot-devtools 时,在热部署的情况下,防止重启报错】
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
8 编写 AccountRealm.java 配置文件
AccountRealm.java
:Realm,【继承 Shiro 内置的 AuthorizingRealm】
/**
* Realm:继承 Shiro 内置的 AuthorizingRealm
*/
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
/**
* supports:让 realm 支持 jwt 的凭证校验,【shiro 默认 supports 是 UsernamePasswordToken,使用 jwt 的方式,需要自定义 JwtToken 取代 UsernamePasswordToken】
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* doGetAuthorizationInfo(授权):权限校验
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* doGetAuthenticationInfo(认证):登录认证,【通过 JwtToken 获取到用户信息 来 判断用户的状态,并抛出对应的异常信息】,同时,【登录成功之后返回的一个用户信息的载体,将 user 信息封装成 SimpleAuthenticationInfo 对象 返回给 shiro】
*/
@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); //将 user对象 拷贝一份至 AccountProfile 对象
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
}
JwtToken.java
:Token,用于 AccountRealm.java 的 supports 方法,【shiro 默认 supports 是 UsernamePasswordToken,使用 jwt 的方式,需要自定义 JwtToken 取代 UsernamePasswordToken】
/**
* Token:用于 AccountRealm.java 的 supports 方法,【shiro 默认 supports 是 UsernamePasswordToken,使用 jwt 的方式,需要自定义 JwtToken 取代 UsernamePasswordToken】
*/
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;
}
}
AccountProfile.java
:Profile,用于 AccountRealm.java 的 AuthenticationInfo 方法,【登录成功之后返回的一个用户信息的载体,将 user 信息封装成 SimpleAuthenticationInfo 对象 返回给 shiro】
/**
* Profile:用于 AccountRealm.java 的 AuthenticationInfo 方法,【登录成功之后返回的一个用户信息的载体,将 user 信息封装成 SimpleAuthenticationInfo 对象 返回给 shiro】
*/
@Data
public class AccountProfile implements Serializable {
private Long id;
private String username;
private String avatar;
private String email;
}
AuthenticatingFilter.java
:源码分析,executeLogin()
执行登录时,会使用 JWT(token),并委托 AccountRealm.java 中的 doGetAuthenticationInfo 方法进行登录认证
public abstract class AuthenticatingFilter extends AuthenticationFilter {
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
} else {
try {
// 使用token,并委托 AccountRealm.java 中的 doGetAuthenticationInfo 方法进行登录认证
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(token, var5, request, response);
}
}
}
}
Part03-集成Shiro-Redis-Jwt实现会话共享(二)
blog-tiny
└─src
└─main
├─java
│ └─org
│ └─org.myslayers
│ │
│ ├─common
│ │ ├─exception
│ │ │ GlobalExceptionHandler.java.java # 全局异常
│ │ │
│ │ └─lang
│ │ Result.java # 统一封装返回结果
│ │
│ ├─config
│ │ CorsConfig.java.java # 跨越问题
│ │
│ ├─entity
│ │ User.java # 表单校验
│ │
│ ├─shiro
│ │ JwtFilter.java # 跨越问题
9 前后端分离:统一封装返回结果
Result.java
:实体类,【前后端分离,统一封装返回结果】
/**
* 统一封装返回结果
*/
@Data
public class Result implements Serializable {
// 操作状态:200代表成功,非200为失败/异常
private int code;
// 携带msg
private String msg;
// 携带data
private Object data;
public static Result success(int code, String msg, Object data) {
Result result = new Result();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static Result success(String msg) {
return Result.success(200, msg, null);
}
public static Result success(String msg, Object data) {
return Result.success(200, msg, data);
}
public static Result fail(int code, String msg, Object data) {
Result result = new Result();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static Result fail(String msg) {
return fail(500, msg, null);
}
public static Result fail(String msg, Object data) {
return fail(500, msg, data);
}
}
10 前后端分离:全局异常
-
默认情况下,返回 Tomcat 或 Nginx 报错页面,对用户不太友好,除此之外,可以根据实际需要对状态码进一步处理。
-
GlobalExceptionHandler.java
:全局异常,【Shiro 抛出的异常、处理实体校验的异常、处理 Assert 的异常、处理 Runtime 异常】
/**
* 全局异常
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//ShiroException:Shiro抛出的异常,比如用户权限、用户登录
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(value = ShiroException.class)
public Result handler(ShiroException e) {
log.error("运行时异常:----------------{}", e);
return Result.fail(401, e.getMessage(), null);
}
//MethodArgumentNotValidException:处理实体校验的异常
@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());
}
//IllegalArgumentException:处理 Assert 的异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:----------------{}", e);
return Result.fail(e.getMessage());
}
//RuntimeException:处理 Runtime 异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:----------------{}", e);
return Result.fail(e.getMessage());
}
}
UserController.java
:控制层,【测试全局异常是否生效,使用 @RequiresAuthentication 注解】
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@RequiresAuthentication //需要用户登录,否则无法访问该接口
@GetMapping({"", "/", "/index", "/index.html"})
public Object index() {
User user = userService.getById(1);
return Result.success(200, "操作成功!", user);
}
}
- 效果图如下:
11 前后端分离:表单校验
-
默认提交表单数据时,前端校验可以使用类似于 jQuery Validate 等 js 插件实现,而后端校验可以使用 Hibernate validatior 来做校验。注:SpringBoot 默认集成 Hibernate validatior 校验
-
User.java
:实体类
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("m_user")
public class User extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 用户的【昵称】
*/
@NotBlank(message = "昵称不能为空")
private String username;
/**
* 用户的【密码】
*/
@NotBlank(message = "密码不能为空")
private String password;
/**
* 用户的【邮件】
*/
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
/**
* 用户的【性别】:0代表男,1代表女
*/
private String gender;
/**
* 用户的【头像】
*/
private String avatar;
/**
* 用户的【状态】:0代表登录成功,-1代表登录失败
*/
private Integer status;
/**
* 用户的【近期登陆日期】
*/
private Date lasted;
}
Post.java
:实体类
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("m_post")
public class Post extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 文章的【用户ID】
*/
private Long userId;
/**
* 文章的【标题】
*/
@NotBlank(message = "标题不能为空")
private String title;
/**
* 文章的【描述】
*/
@NotBlank(message = "描述不能为空")
private String description;
/**
* 文章的【内容】
*/
@NotBlank(message = "内容不能为空")
private String content;
/**
* 文章的【状态】
*/
private Integer status;
}
UserController.java
:控制层,【测试表单校验是否生效,使用 @Validated 注解】
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@PostMapping("/save")
public Object testUser(@Validated @RequestBody User user) {
return user.toString();
}
}
- 效果图如下:
12 前后端分离:跨域问题
CorsConfig.java
:配置类,【前后端分离,跨域问题】
/**
* 前后端分离:跨域问题
*/
@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("*");
}
}
JwtFilter.java
:配置类,【前后端分离,拦截器中加入跨域支持,这样拦截器才不会在进入 Controller 之前就被限制了】
/**
* Filter:继承 Shiro 内置的 AuthenticatingFilter
*/
@Component
public class JwtFilter extends AuthenticatingFilter {
/**
* preHandle:拦截器的前置拦截,【前后端项目,除了需要跨域全局配置之外,拦截器中也需要提供跨域支持。这样,拦截器才不会在进入 Controller 之前就被限制了】
*/
@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);
}
}
Part04-使用Shiro-Redis-Jwt开发登录接口
blog-tiny
└─src
└─main
├─java
│ └─org
│ └─org.myslayers
│ ├─controller
│ │ UserController.java # 登录接口
│ │
│ └─utils
│ ValidationUtil.java # 工具类
13 集成 ValidationUtil 工具类
ValidationUtil.java
:工具类,【常见校验】
/**
* ValidationUtil 工具类
*/
@Component
public class ValidationUtil {
/**
* 开启快速结束模式 failFast (true)
*/
private static Validator validator = Validation.byProvider(HibernateValidator.class).configure()
.failFast(false).buildValidatorFactory().getValidator();
/**
* 校验对象
*
* @param t bean
* @param groups 校验组
* @return ValidResult
*/
public static <T> ValidResult validateBean(T t, Class<?>... groups) {
ValidResult result = new ValidationUtil().new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(violation.getPropertyPath().toString(), violation.getMessage());
}
}
return result;
}
/**
* 校验bean的某一个属性
*
* @param obj bean
* @param propertyName 属性名称
* @return ValidResult
*/
public static <T> ValidResult validateProperty(T obj, String propertyName) {
ValidResult result = new ValidationUtil().new ValidResult();
Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
boolean hasError = violationSet != null && violationSet.size() > 0;
result.setHasErrors(hasError);
if (hasError) {
for (ConstraintViolation<T> violation : violationSet) {
result.addError(propertyName, violation.getMessage());
}
}
return result;
}
/**
* 校验结果类
*/
@Data
public class ValidResult {
/**
* 是否有错误
*/
private boolean hasErrors;
/**
* 错误信息
*/
private List<ErrorMessage> errors;
public ValidResult() {
this.errors = new ArrayList<>();
}
public boolean hasErrors() {
return hasErrors;
}
public void setHasErrors(boolean hasErrors) {
this.hasErrors = hasErrors;
}
/**
* 获取所有验证信息
*
* @return 集合形式
*/
public List<ErrorMessage> getAllErrors() {
return errors;
}
/**
* 获取所有验证信息
*
* @return 字符串形式
*/
public String getErrors() {
StringBuilder sb = new StringBuilder();
for (ErrorMessage error : errors) {
sb.append(error.getPropertyPath()).append(":").append(error.getMessage())
.append(" ");
}
return sb.toString();
}
public void addError(String propertyName, String message) {
this.errors.add(new ErrorMessage(propertyName, message));
}
}
@Data
public class ErrorMessage {
private String propertyPath;
private String message;
public ErrorMessage() {
}
public ErrorMessage(String propertyPath, String message) {
this.propertyPath = propertyPath;
this.message = message;
}
}
}
14 集成 Shiro-Redis、Jwt 开发登录接口
UserController.java
:控制层,【用户登录/登出】
@RestController
public class UserController extends BaseController {
@Autowired
UserService userService;
@Autowired
JwtUtils jwtUtils;
/*--------------------------------------1.用户登录/登出------------------------------------>*/
@ResponseBody
@PostMapping("/login")
public Result login(@RequestBody Map<String, String> map) {
//使用Map对象接收一个json对象
String username = map.get("username");
String password = map.get("password");
//判断输入是否为空
if (StrUtil.isEmpty(username) || StrUtil.isBlank(password)) {
return Result.fail("账号或密码不能为空");
}
//根据username查询该用户
User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
//判断用户是否存在
Assert.notNull(user, "用户不存在!");
//判断密码是否正确
if (!user.getPassword().equals(SecureUtil.md5(password))) {
return Result.fail("密码不正确!");
}
//登录成功后,根据 用户id 生成 jwt token,并将 jwt 返回至 response 的 header 请求头中
String jwt = jwtUtils.generateToken(user.getId());
resp.setHeader("Authorization", jwt);
resp.setHeader("Access-control-Expose-Headers", "Authorization");
return Result.success("登录成功!", 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.success("退出登录!");
}
}
- 效果图如下:
- 效果图如下:
Part05-使用Shiro-Redis-Jwt开发项目接口
blog-tiny
└─src
└─main
├─java
│ └─org
│ └─org.myslayers
│ ├─controller
│ │ UserController.java # 项目接口
15 集成 Shiro-Redis、Jwt 开发项目接口
@RestController
public class PostController extends BaseController {
@Autowired
PostService PostService;
/**
* 【查询】全部文章
*/
@GetMapping("/post/list")
public Result list(@RequestParam(defaultValue = "1") Integer currentPage) {
IPage pageData = PostService.page(new Page(currentPage, 5), new QueryWrapper<Post>().orderByDesc("created"));
return Result.success("操作成功!", pageData);
}
/**
* 【查看】文章
*/
@GetMapping("/post/{id}")
public Result detail(@PathVariable(name = "id") Long id) {
Post post = PostService.getById(id);
Assert.notNull(post, "该博客已被删除");
return Result.success("操作成功", post);
}
/**
* 【更新/添加】文章
*/
@RequiresAuthentication
@PostMapping("/post/edit")
public Result edit(@Validated @RequestBody Post post) {
Post temp = null;
/**
* 编辑文章:存在文章id,则判断【是否拥有权限编辑该篇文章】
*/
if (post.getId() != null) {
// 登录成功之后返回的登录用户的id 与 当前操作的文章的用户id 比较,判断【是否拥有权限编辑该篇文章】
temp = PostService.getById(post.getId());
Assert.isTrue(temp.getUserId().longValue() == getProfile().getId().longValue(), "没有权限编辑该篇文章!");
} else {
/**
* 添加文章:不存在文章id,则进行字段补充,将post添加至数据库
*/
// 对传入的post文章,进行字段补充,比如userId、status、created
temp = new Post();
temp.setUserId(getProfile().getId());
temp.setStatus(0);
temp.setCreated(new Date());
temp.setModified(new Date());
}
// 从 post 拷贝至 temp,忽略"id", "userId", "created", "status"
BeanUtil.copyProperties(post, temp, "id", "userId", "created", "status", "modified");
PostService.saveOrUpdate(temp);
return Result.success("操作成功!", temp);
}
}
- 效果图如下:
- 效果图如下:
- 效果图如下:
Part06-完成项目搭建-vuecli、elementui、axios
blog-tiny-vue
│ .gitignore
│ babel.config.js
│ blog-tiny-vue.iml
│ package-lock.json
│ package.json
│ README.md
│
├─node_modules
│
├─public
│ favicon.ico
│ index.html
│
└─src
│ App.vue
│ main.js
│
├─assets
│ logo.png
│
├─components
│ HelloWorld.vue
│
├─router
│ index.js
│
├─store
│ index.js
│
└─views
About.vue
Home.vue
16 使用 vuecli 完成项目搭建
- 初始化项目,命令如下:
a.安装
npm install -g @vue/cli && npm install -g webpack && npm install -g webpack-cli
b.版本
vue -V && webpack -v
@vue/cli 4.5.12、webpack 5.30.0、webpack-cli 4.6.0
c.创建
cd D:\software_ware\workspace_idea\blog-tiny && vue ui
d.相关配置
项目名称:blog-tiny-vue
包管理器:npm
Git版本控制:关闭
手动配置项目:Choose vue version(勾选)、Babel(勾选)、Router(勾选)、Vuex(勾选)、Linter/ Formatter(取消勾选)
功能及配置:Vue.js使用2.x(默认)、是否使用旧的路由模式(是)
保存为新预设:暂不设置
17 使用 elementui 完成项目搭建
- 初始化项目,命令如下:
a.安装
cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
cnpm install element-ui --save # 安装 ElementUi
yarn add element-ui # 安装 ElementUi
b.配置
cd src/mian.js
------------------------------------------------------------
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Element from 'element-ui' # 添加该行
import "element-ui/lib/theme-chalk/index.css" # 添加该行
Vue.config.productionTip = false;
Vue.use(Element); # 添加该行
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
18 使用 axios 完成项目搭建
- 初始化项目,命令如下:
a.安装
cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
cnpm install axios --save # 安装 Axios
yarn add axios # 安装 Axios
b.配置
cd src/mian.js
------------------------------------------------------------
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
import axios from 'axios' # 添加该行
Vue.config.productionTip = false;
Vue.use(Element);
Vue.prototype.$axios = axios # 添加该行
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
Part07-实现用户登录-页面渲染、路由规则、状态管理、全局响应拦截、全局前置守卫
blog-tiny-vue
│
└─src
│ App.vue
│ axios.js # 2.4 用户登录 - 全局响应拦截
│ main.js
│ permission.js # 2.5 用户登录 - 全局前置守卫
│
├─router
│ index.js # 2.2 用户登录 - 路由规则
│
├─store
│ index.js # 2.3 用户登录 - 状态管理
│
└─views
Login.vue # 2.1 用户登录 - 页面渲染
19 用户登录 - 页面渲染
/views/Login.vue
:页面渲染
<template>
<div>
<el-container>
<el-header>
<img class="logo" src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png">
</el-header>
<el-main>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
ruleForm: {
username: 'admin',
password: '123456',
},
rules: {
username: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur'}
],
password: [
{required: true, message: '请输入密码', trigger: 'change'},
],
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
// axios.js 中 axios.defaults.baseURL + /login 等同于 http://localhost:8765/login
this.$axios.post('/login', this.ruleForm).then(res => {
//请求后的数据
console.log(res.data)
console.log(res.headers)
const token = res.headers['authorization']
const userInfo = res.data.data
//调用vuex中的set方法
_this.$store.commit("SET_TOKEN", token);
_this.$store.commit("SET_USERINFO", userInfo);
//获取vuex中的get方法
console.log(_this.$store.getters.GET_TOKEN);
console.log(_this.$store.getters.GET_USERINFO);
console.log(_this.$store.getters.GET_USERINFO.avatar);
console.log(_this.$store.getters.GET_USERINFO.username);
//跳转页面
_this.$router.push("/");
})
} else {
console.log('登录失败!!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
/*----------------自定义样式----------------*/
.logo {
height: 60%;
margin-top: 10px;
}
.demo-ruleForm {
max-width: 500px;
margin: 0 auto;
}
</style>
20 用户登录 - 路由规则
/router/index.js
:路由规则
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
import PostList from "@/views/PostList";
import PostDetail from "@/views/PostDetail";
import PostEdit from "@/views/PostEdit";
Vue.use(VueRouter);
const routes = [
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/',
name: 'Index',
redirect: {name: "PostList"} /*重定向至 Blogs 组件*/
},
{
path: '/post/list',
name: 'PostList',
component: PostList /*主页【查看全部文章】*/
},
{
path: '/post/add',
name: 'PostAdd',
component: PostEdit, /*发表【新建一篇文章】*/
meta: {
requireAuth: true /*对应【权限拦截】自定义规则*/
}
},
{
path: '/post/:postId',
name: 'PostDetail',
component: PostDetail /*详情【查看某篇文章】*/
},
{
path: '/post/:postId/edit',
name: 'PostEdit',
component: PostEdit, /*编辑【编辑某篇文章】*/
meta: {
requireAuth: true /*对应【权限拦截】自定义规则*/
}
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
21 用户登录 - 状态管理
/store/index.js
:状态管理,【用途一:将 token、userInfo 存储到 vuex 中供其他组件调用;用途二:将其存储到 localStorage 中供下次打开浏览器使用】
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
//attr属性
state: {
token: localStorage.getItem("token"),
userInfo: JSON.parse(localStorage.getItem("userInfo"))
},
//set方法
mutations: {
SET_TOKEN: (state, token) => {
state.token = token;
localStorage.setItem("token", token)
},
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo;
localStorage.setItem("userInfo", JSON.stringify(userInfo))
},
REMOVE_ALL: (state) => {
state.token = ''
state.userInfo = {}
localStorage.setItem("token", '')
localStorage.setItem("userInfo", JSON.stringify(''))
},
},
//get方法
getters: {
GET_TOKEN: state => {
return state.token;
},
GET_USERINFO: state => {
return state.userInfo;
}
},
actions: {},
modules: {}
})
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
//attr属性
state: {
token: localStorage.getItem("token"),
userInfo: JSON.parse(localStorage.getItem("userInfo"))
},
//set方法
mutations: {
SET_TOKEN: (state, token) => {
state.token = token;
localStorage.setItem("token", token)
},
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo;
localStorage.setItem("userInfo", JSON.stringify(userInfo))
},
REMOVE_ALL: (state) => {
state.token = ''
state.userInfo = {}
localStorage.setItem("token", '')
localStorage.setItem("userInfo", JSON.stringify(''))
},
},
//get方法
getters: {
GET_TOKEN: state => {
return state.token;
},
GET_USERINFO: state => {
return state.userInfo;
}
},
actions: {},
modules: {}
})
22 用户登录 - 全局响应拦截
axios.js
:编写【全局响应拦截】,【对应GlobalExceptionHandler.java
全局异常】
import axios from 'axios'
import Element from 'element-ui'
axios.defaults.baseURL = "http://localhost:8765"
/**
* 前置拦截
*/
axios.interceptors.request.use(request => {
return request
})
/**
* 后置拦截
*/
axios.interceptors.response.use(
//状态码错误
succ => {
//状态码,200
if (succ.data.code === 200) {
return succ
}
//状态码,非200
if (succ.data.code !== 200) {
var mess = succ.data.msg;
Element.Message.error(mess, {duration: 3 * 1000}) //消息弹窗
return Promise.reject(mess)
}
},
//程序运行错误
error => {
//处理Assert的异常,比如:Assert.notNull(user, "用户不存在!");
if (error.response.data) {
var mess = error.response.data.msg;
Element.Message.error(mess, {duration: 3 * 1000}) //消息弹窗
return Promise.reject(mess)
}
//处理Shiro的异常,比如:用户权限、用户登录
if (error.response.status === 401) {
this.$store.commit("REMOVE_ALL")
this.$router.push("/login")
}
}
)
main.js
:使用【全局响应拦截】,【直接将编写好的 axios.js 导入 main.js】
import "./axios" /*axios全局拦截:前置拦截、后置拦截*/
23 用户登录 - 全局前置守卫
permission.js
:编写【全局前置守卫】,【对应/router/index.js
路由规则】
import router from "./router";
//权限拦截:根据【/router/index.js】中的路由规则,是否需要进行【权限验证:登录】
router.beforeEach((to, from, next) => {
//判断该路由是否需要登录权限
if (to.matched.some(record => record.meta.requireAuth)) {
const token = localStorage.getItem("token")
console.log(token)
//判断当前的token是否存在
if (token) {
if (to.path === '/login') {
} else {
next()
}
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
main.js
:使用【全局前置守卫】,【直接将编写好的 permission.js 导入 main.js】
import "./permission" /*全局前置守卫*/
Part08-实现博客页面-公共组件、博客列表、博客发表、博客编辑、博客详情
blog-tiny-vue
│
└─src
├─components
│ Header.vue # 2.1 公共组件
│
└─views
Login.vue
PostDetail.vue # 2.4 博客详情
PostEdit.vue # 2.3 博客发表 / 博客编辑
PostList.vue # 2.2 博客列表
24 博客页面 - 公共组件
/components/Header.vue
:编写【公共组件】
<template>
<div style="max-width: 960px; margin: 0 auto; text-align: center;">
<h3>欢迎来到Halavah的博客!</h3>
<div class="block">
<el-avatar :size="50" :src="user.avatar"></el-avatar>
<div>{{ user.username }}</div>
</div>
<div style="margin: 10px 0;">
<span>
<el-link href="/blogs">主页</el-link>
</span>
<el-divider direction="vertical"></el-divider>
<span><el-link type="success" href="/blog/add">发表博客</el-link></span>
<el-divider direction="vertical"></el-divider>
<span v-show="!isLogin"><el-link type="primary" href="/login">登录</el-link></span>
<span v-show="isLogin"><el-link type="danger" @click="logout()">退出</el-link></span>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
user: {
username: '',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
},
isLogin: false
}
},
methods: {
logout() {
const _this = this
_this.$axios.get("/logout", {
headers: {
"authorization": this.$store.getters.GET_TOKEN
},
}).then(res => {
_this.$store.commit("REMOVE_ALL")
_this.$router.push("/login")
})
}
},
created() {
if(this.$store.getters.GET_USERINFO.username) {
this.user.username = this.$store.getters.GET_USERINFO.username
this.user.avatar = this.$store.getters.GET_USERINFO.avatar
this.isLogin = true
}
}
}
</script>
<style>
</style>
/views/PostList.vue
:使用【公共组件】
<template>
<div>
<Header></Header>
</div>
</template>
<script>
import Header from "@/components/Header";
export default {
name: "PostList",
components: {Header},
}
</script>
<style>
</style>
25 博客页面 - 博客列表
/views/PostList.vue
:博客列表
<template>
<div>
<Header></Header>
<div>
<div class="block">
<!--时间线-->
<el-timeline>
<el-timeline-item :timestamp="post.created" placement="top" v-for="(post, index) in posts" :key="index">
<el-card>
<h4>
<!--使用router-link来跳转到某个组件,例如跳转到【BlogDetail组件】,需要传递参数【blogId: post.id】-->
<router-link :to="{name: 'PostDetail', params: {postId: post.id}}">{{ post.title }}</router-link>
</h4>
<p>{{ post.description }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
<!--分页-->
<el-pagination style="margin: 0 auto; text-align: center" background layout="prev, pager, next" :current-page="currentPage" :page-size="pageSize" :total="total" @current-change=handleCurrentChange></el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/Header";
export default {
name: "PostList",
components: {Header},
data() {
return {
posts: {},
currentPage: 1,
total: 0,
pageSize: 5
}
},
methods: {
handleCurrentChange(currentPage) {
const _this = this
_this.$axios.get("/post/list?currentPage=" + currentPage).then(res => {
console.log(res)
_this.posts = res.data.data.records
_this.currentPage = res.data.data.current
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
//页面一开始渲染时,调用mounted()方法
mounted() {
this.handleCurrentChange(1)
}
}
</script>
<style>
</style>
26 博客页面 - 博客发表 / 博客编辑
- 基于 Vue 的 markdown 编辑器(编写):mavon-editor
a.安装
cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
cnpm install mavon-editor --save # 用于编写md文档
yarn add mavon-editor # 用于编写md文档
b.配置
cd src/mian.js
------------------------------------------------------------
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
import mavonEditor from 'mavon-editor' # 添加该行
import 'mavon-editor/dist/css/index.css' # 添加该行
import axios from 'axios'
import "./axios" /*axios全局拦截:前置拦截、后置拦截*/
Vue.config.productionTip = false;
Vue.use(Element);
Vue.prototype.$axios = axios;
Vue.use(mavonEditor); # 添加该行
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
c.使用
/views/PostEdit.vue
------------------------------------------------------------
<el-form-item label="内容" prop="content">
<mavon-editor v-model="ruleForm.content"></mavon-editor>
</el-form-item>
/views/PostEdit.vue
:博客发表 / 博客编辑
<template>
<div>
<Header></Header>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" style="max-width: 80%;">
<el-form-item label="标题" prop="title">
<el-input v-model="ruleForm.title"></el-input>
</el-form-item>
<el-form-item label="摘要" prop="description">
<el-input type="textarea" v-model="ruleForm.description"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<mavon-editor v-model="ruleForm.content"></mavon-editor>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">发布</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import Header from "@/components/Header";
export default {
name: "PostEdit",
components: {Header},
data() {
return {
ruleForm: {
id: '',
title: '',
description: '',
content: ''
},
rules: {
title: [
{required: true, message: '请输入标题', trigger: 'blur'},
{min: 3, max: 25, message: '长度在 3 到 25 个字符', trigger: 'blur'}
],
description: [
{required: true, message: '请输入摘要', trigger: 'blur'}
],
content: [
{required: true, message: '请输入内容', trigger: 'blur'}
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/post/edit', this.ruleForm, {headers: {"authorization": localStorage.getItem("token")}})
.then(res => {
console.log(res)
_this.$alert('操作成功', '提示', { /*MessageBox 弹框*/
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/post/list")
}
});
})
} else {
console.log('操作失败!!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
//页面一开始渲染时,调用mounted()方法(回写操作)
mounted() {
/*根据 /router/index.js 路由规则:【path: '/post/:postId/edit'】中包含参数postId*/
const postId = this.$route.params.postId
console.log(postId)
const _this = this
if (postId) {
this.$axios.get('/post/' + postId).then(res => {
const post = res.data.data
_this.ruleForm.id = post.id
_this.ruleForm.title = post.title
_this.ruleForm.description = post.description
_this.ruleForm.content = post.content
})
}
}
}
</script>
<style>
</style>
27 博客页面 - 博客详情
- 基于 Vue 的 markdown 编辑器(解析):markdown-it、github-markdown-css
a.安装
cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
cnpm install markdown-it --save # 用于解析md文档
cnpm install github-markdown-css # 用于解析md样式
yarn add markdown-it # 用于解析md文档
yarn add github-markdown-css # 用于解析md样式
b.配置
无
c.使用
/views/PostDetail.vue
------------------------------------------------------------
<template>
<div>
<div>
<!--使用【解析md样式、解析md文档】-->
<div class="markdown-body" v-html="post.content"></div>
</div>
</div>
</template>
<script>
/*配置【解析md样式】*/
import 'github-markdown-css'
export default {
//页面一开始渲染时,调用mounted()方法(回写操作)
mounted() {
/*根据 /router/index.js 路由规则:【path: '/post/:postId/edit'】中包含参数postId*/
const postId = this.$route.params.postId
console.log(postId)
const _this = this
this.$axios.get('/post/' + postId).then(res => {
const post = res.data.data
_this.post.id = post.id
_this.post.title = post.title
/*配置【解析md文档】*/
var MardownIt = require("markdown-it")
var md = new MardownIt()
var result = md.render(post.content)
_this.post.content = result
_this.editButton = (post.userId === _this.$store.getters.GET_USERINFO.id)
})
}
}
</script>
/views/PostDetail.vue
:博客详情
<template>
<div>
<Header></Header>
<div style="box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); width: 100%; min-height: 700px; padding: 20px 15px;">
<h2> {{ post.title }} </h2>
<!--编辑按钮-->
<el-link icon="el-icon-edit" v-if="editButton">
<!--使用router-link来跳转到某个组件,例如跳转到【BlogDetail组件】,需要传递参数【blogId: post.id】-->
<router-link :to="{name: 'PostEdit', params: {postId: post.id}}">编辑</router-link>
</el-link>
<!--分割线-->
<el-divider></el-divider>
<!--使用【解析md样式、解析md文档】-->
<div class="markdown-body" v-html="post.content"></div>
</div>
</div>
</template>
<script>
/*配置【解析md样式】*/
import 'github-markdown-css'
import Header from "@/components/Header";
export default {
name: "PostDetail",
components: {Header},
data() {
return {
post: {
id: "",
title: "",
content: ""
},
editButton: false
}
},
//页面一开始渲染时,调用mounted()方法(回写操作)
mounted() {
/*根据 /router/index.js 路由规则:【path: '/post/:postId/edit'】中包含参数postId*/
const postId = this.$route.params.postId
console.log(postId)
const _this = this
this.$axios.get('/post/' + postId).then(res => {
const post = res.data.data
_this.post.id = post.id
_this.post.title = post.title
/*解析md文档*/
var MardownIt = require("markdown-it")
var md = new MardownIt()
var result = md.render(post.content)
_this.post.content = result
_this.editButton = (post.userId === _this.$store.getters.GET_USERINFO.id)
})
}
}
</script>
<style>
</style>