<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
需要添加一个配置文件,这样热重启时才不会报错
resource/META-INF/spring-devtools.properties
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
CREATE DATABASE`vueblog`;
USE `vueblog`;
DROP TABLE IF EXISTS `m_blog`;
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=15 DEFAULT CHARSET=utf8mb4;
insert into `m_blog`(`id`,`user_id`,`title`,`description`,`content`,`created`,`status`) values
(1,1,'生活就像海洋,只有意志坚强的人才能到达彼岸','这里是摘要哈哈哈','内容???','2020-05-21 22:08:42',0),
(2,1,'尊重','我以为别人尊重我是因为我很优秀,后来慢慢才明白,别人尊重我,是因为别人很优秀。','内容:我以为别人尊重我是因为我很优秀,后来慢慢才明白,别人尊重我,是因为别人很优秀。','2022-08-22 22:08:42',0);
DROP TABLE IF EXISTS `m_user`;
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 AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `m_user`(`id`,`username`,`avatar`,`email`,`password`,`status`,`created`,`last_login`) values
/*密码是MD5加密后的*/
(1,'root','https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg',NULL,'96e79218965eb72c92a549dd5a330112',0,'2020-04-20 10:44:01',NULL);
application.yml
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--页面模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
application.yml
mybatis-plus:
mapper-locations:classpath*:/mapper/**Mapper.xml
com.example.config.MybatisPlusConfig
package com.example.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.example.mapper") //开启mapper接口扫描
public class MybatisPlusConfig {
//添加分页插件
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
com.example.CodeGenerator.java
package com.example;
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.isNotEmpty(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("xqh");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
// 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("com.example");
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();
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("m_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
com.example.controller.UserController.java
@RestController //返回的是json
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
public Object test(@PathVariable("id") Long id) {
return userService.getById(id);
}
}
package com.example.common.lang;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result implements Serializable {
private int code;
private String msg;
private Object data;
//封装succ方法,因为一般情况下操作成功code,msg都是固定,只有返回对象不同
public static Result succ(Object data){
return succ(200,"操作成功",data);
}
public static Result succ(int code,String msg,Object data){
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
//封装fail方法,返回400固定,给的错误信息不同
public static Result fail(String msg){
return succ(400,msg,null);
}
//封装fail方法,返回400固定,给的错误信息和返回的对象不同
public static Result fail(String msg,Object data){
return fail(400,msg,data);
}
public static Result fail(int code,String msg,Object data){
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
}
在UserController中
@GetMapping("/index")
public Result getById(){
User user = userService.getById(1L);
return Result.succ(user);
}
<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>
shiro-redis-spring-boot-starter :快速整合 shiro-redis
hutool-all : Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。
Hutool 中的工具方法来自每个用户的精雕细琢,它涵盖了 Java 开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;一个 Java 基础工具类,对文件、流、加密、解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类
jjwt :通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token
,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:
POST
请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探JWT Token
,形成的JWT Token
就是一个如同lll.zzz.xxx
的字符串JWT Token
字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token
即可JWT Token
,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等JWT Token
中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果jwt就是利用token进行用户身份验证时token的一种具体实现方式。 客户端使用用户名和密码登录,服务端收到请求,验证用户名和密码,验证成功后经过一系列操作(将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名)形成一个JwtToken 返回到客户端 。客户端每次请求资源时都需要袖带这个jwtToken ,服务端收到请求再去验证JwtToken的有效性等。
application.yml
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
root:
jwt:
secret: f4e2e52034348f86b67cde581c0f9eb5
expire: 604800
header: Authorization
com.example.config.ShrioConfig.java
package com.example.config;
import com.example.shiro.AccountRealm;
import com.example.shiro.JwtFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
@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);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
@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 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,我们主要做了几件事情:
com.example.shiro.AccountRealm.java
package com.example.shiro;
import cn.hutool.core.bean.BeanUtil;
import com.example.entity.User;
import com.example.service.UserService;
import com.example.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;
//为了让Realm支持jwt的凭证校验
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//权限校验
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//登录认证校验
@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); //copy,把user对象里的东西copy到profile里面
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
}
com.example.shiro.JwtToken.java
package com.example.shiro;
import org.apache.shiro.authc.AuthenticationToken;
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;
}
}
com.example.util.JwtUtils.java
package com.example.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 = "markerhub.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());
}
}
com.example.shiro.AccountProfile.java
package com.example.shiro;
import lombok.Data;
import java.io.Serializable;
//把一些可以公共的信息封装起来 ,除去密码
@Data
public class AccountProfile implements Serializable {
private Long id;
private String username;
private String avatar;
private String email;
}
com.example.shiro.JwtFilter.java
package com.example.shiro;
import cn.hutool.json.JSONUtil;
import com.example.common.lang.Result;
import com.example.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@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)) {
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);
}
}
//执行登录异常进行处理
@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;
}
@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 进行身份校验!
com.example.exception.GlobalExceptionHandler.java
package com.example.common.exception;
import com.example.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;
@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());
}
}
上面我们捕捉了几个异常:
表单数据提交时,后端可以用Hibernate validatior 来做校验。
我们使用 springboot 框架作为基础,那么就已经自动集成了 Hibernate validatior。(我用的springboot版本是2.2.6.RELEASE,高版本没有集成这个 Hibernate validatior,如果需要使用实体校验,需要自己导入 Hibernate validatior依赖,可以去maven仓库搜索导入依赖)
@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;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
...
}
/**
* 测试实体校验
* @param user
* @return
*/
@PostMapping("/save")
public Object testUser(@Validated @RequestBody User user) {
return user.toString();
}
@Validated 是关键,只有加上这个注解,实体校验才会生效哦!
com.example.config.CorsConfig.java
package com.example.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("*");
}
}
至sringboot后端框架就已经搭建好了,下面就是开始编写业务接口了
com.example.controller.AccountController.java
package com.example.controller;
import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.common.dto.LoginDto;
import com.example.common.lang.Result;
import com.example.entity.User;
import com.example.service.UserService;
import com.example.util.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
@RestController
public class AccountController {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
@PostMapping("/login")
public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
Assert.notNull(user, "用户不存在");
if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
return Result.fail("密码不正确");
}
String jwt = jwtUtils.generateToken(user.getId());
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);
}
}
封装一些属性,用户名和密码
package com.example.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;
}
com.example.controlelr.BlogController.java
package com.example.controller;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.common.lang.Result;
import com.example.entity.Blog;
import com.example.service.BlogService;
import com.example.util.ShiroUtil;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
@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);
}
//根据id查询博客内容
@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 edit(@Validated @RequestBody Blog blog) {
Blog temp = null;
//讨论是编辑自己原来的文章,还是新添加文章
if(blog.getId() != null) {
//不是空,就先获取原来的文章(update)
temp = blogService.getById(blog.getId());
// 只能编辑自己的文章
System.out.println(ShiroUtil.getProfile().getId());
//判断编辑的人是不是原来的用户,如果不是:没有权限编辑
Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(), "没有权限编辑");
} else {
//add添加方法
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.succ(null);
}
}
至此后端部分就全部完成
1.该有的nodejs 、vue都要安装上,我用的是vuecli3,所以可以使用可视化界面来创建项目
2.cmd命令行输入: vue ui
3.开始创建一个vue项目脚手架,插件可以先不勾选,到时候用什么再下,这里我使用的是vue2.0,而不是新版的3,因为3是最新版的有很多功能还不太熟悉,很多组件下载也不一样
4.创建好项目后,用idea打开这个项目 vue-blog
在idea打开的这个vue项目中,终端输入cnpm install element-ui --save
下载好后,在main.js中配置依赖
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)
这样就可以使用elementui这个组件了
输入cnpm install axios --save
下载后在main.js中配置
import axios from 'axios'
Vue.prototype.$axios = axios
组件中,我们就可以通过 this.$axios.get () 来发起我们的请求了,和后端交互
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import BlogDetail from "@/views/BlogDetail";
import BlogEdit from "@/views/BlogEdit";
import Login from "@/views/Login";
import Blogs from "@/views/Blogs";
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
redirect: { name: 'Blogs' }
},
{
path: '/blogs',
name: 'Blogs',
component: Blogs
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
name: 'BlogAdd',
meta: {
requireAuth: true //需要登录才能访问
},
component: BlogEdit //博客编辑页 添加博客add
},
{
path: '/blog/:blogId',
name: 'BlogDetail',
component: BlogDetail //博客详情页
},
{
path: '/blog/:blogId/edit',
name: 'BlogEdit',
meta: {
requireAuth: true
},
component: BlogEdit //博客编辑页 这个是修改博客 update
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
其中,带有 meta:requireAuth: true 说明是需要登录字后才能访问的受限资源,后面我们路由权限拦截时候会用到。
views/Login.vue
<template>
<div>
<el-container>
<el-header>
<img class="mlogo" src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png" alt="">
</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: 'markerhub',
password: '111111'
},
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
this.$axios.post('/login', this.ruleForm).then(res => {
console.log(res.data)
const jwt = res.headers['authorization']
const userInfo = res.data.data
// 把数据共享出去
_this.$store.commit("SET_TOKEN", jwt)
_this.$store.commit("SET_USERINFO", userInfo)
// 获取
console.log(_this.$store.getters.getUser)
_this.$router.push("/blogs")
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped>
.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 {
/*background-color: #E9EEF3;*/
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;
}
.mlogo {
height: 60%;
margin-top: 10px;
}
.demo-ruleForm {
max-width: 500px;
margin: 0 auto;
}
</style>
从返回的结果请求头中获取到 token 的信息,然后使用 store 提交 token 和用户信息的状态。完成操作之后,我们跳转到了 /blogs 路由,即博客列表页面。
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: '',
userInfo: JSON.parse(sessionStorage.getItem("userInfo"))
},
mutations: {
// set
SET_TOKEN: (state, token) => {
state.token = token
localStorage.setItem("token", token)
},
SET_USERINFO: (state, userInfo) => {
state.userInfo = userInfo
sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
},
REMOVE_INFO: (state) => {
state.token = ''
state.userInfo = {}
localStorage.setItem("token", '')
sessionStorage.setItem("userInfo", JSON.stringify(''))
}
},
getters: {
// get
getUser: state => {
return state.userInfo
}
},
actions: {
},
modules: {
}
})
存储 token,我们用的是 localStorage,存储用户信息,我们用的是 sessionStorage。毕竟用户信息我们不需要长久保存,保存了 token 信息,我们随时都可以初始化用户信息。
axios.js
import axios from 'axios'
import Element from 'element-ui'
import router from './router'
import store from './store'
//服务端的接口的链接
axios.defaults.baseURL = "http://localhost:8081"
// 前置拦截
axios.interceptors.request.use(config => {
return config
})
axios.interceptors.response.use(response => {
let res = response.data;
console.log("=================")
console.log(res)
console.log("=================")
if (res.code === 200) {
return response
} else {
Element.Message.error('错了哦,这是一条错误消息', {duration: 3 * 1000})
return Promise.reject(response.data.msg)
}
},
error => {
console.log(error)
if(error.response.data) {
error.message = error.response.data.msg
}
if(error.response.status === 401) {
store.commit("REMOVE_INFO")
router.push("/login")
}
Element.Message.error(error.message, {duration: 3 * 1000})
return Promise.reject(error)
}
)
因为后端返回的是状态码,200或400之类,这里做了配置,返回的是信息
import ‘./axios.js’ // 请求拦截
<template>
<div class="m-content">
<h3>欢迎来到xqh的博客</h3>
<div class="block">
<el-avatar :size="50" :src="user.avatar"></el-avatar>
<div>{{ user.username }}</div>
</div>
<div class="maction">
<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="!hasLogin"><el-link type="primary" href="/login">登录</el-link></span>
<span v-show="hasLogin"><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'
},
hasLogin: false
}
},
methods: {
logout() {
const _this = this
_this.$axios.get("/logout", {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then(res => {
_this.$store.commit("REMOVE_INFO")
_this.$router.push("/login")
})
}
},
created() {
if(this.$store.getters.getUser.username) {
this.user.username = this.$store.getters.getUser.username
this.user.avatar = this.$store.getters.getUser.avatar
this.hasLogin = true
}
}
}
</script>
<style scoped>
.m-content {
max-width: 960px;
margin: 0 auto;
text-align: center;
}
.maction {
margin: 10px 0;
}
</style>
import Header from "@/components/Header";
data() {
components: {Header}
}
# 然后模板中调用组件
<header></header>
views/Blogs.vue
<template>
<div class="mcontaner">
<Header></Header>
<div class="block">
<el-timeline>
<el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs">
<el-card>
<h4>
<router-link :to="{name: 'BlogDetail', params: {blogId: blog.id}}">
{{blog.title}}
</router-link>
</h4>
<p>{{blog.description}}</p>
</el-card>
</el-timeline-item>
</el-timeline>
<el-pagination class="mpage"
background
layout="prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change=page>
</el-pagination>
</div>
</div>
</template>
<script>
import Header from "../components/Header";
export default {
name: "Blogs.vue",
components: {Header},
data() {
return {
blogs: {},
currentPage: 1,
total: 0,
pageSize: 5 //分页信息 每页五条信息
}
},
methods: {
page(currentPage) { //根据当前页码来展示博客列表
const _this = this
_this.$axios.get("/blogs?currentPage=" + currentPage).then(res => {
console.log(res)
_this.blogs = res.data.data.records
_this.currentPage = res.data.data.current
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
created() {
this.page(1) //先展示第一页
}
}
</script>
<style scoped>
.mpage {
margin: 0 auto;
text-align: center;
}
</style>
cnpm install mavon-editor --save
// 全局注册
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)
views/BlogEdit.vue
<template>
<div>
<Header></Header>
<div class="m-content">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<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>
</div>
</template>
<script>
import Header from "../components/Header";
export default {
name: "BlogEdit.vue",
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: [
{ trequired: true, message: '请输入内容', trigger: 'blur' }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/blog/edit', this.ruleForm, {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then(res => {
console.log(res)
_this.$alert('操作成功', '提示', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/blogs")
}
});
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
created() {
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
if(blogId) {
this.$axios.get('/blog/' + blogId).then(res => {
const blog = res.data.data
_this.ruleForm.id = blog.id
_this.ruleForm.title = blog.title
_this.ruleForm.description = blog.description
_this.ruleForm.content = blog.content
})
}
}
}
</script>
<style scoped>
.m-content {
text-align: center;
}
</style>
# 用于解析md文档
cnpm install markdown-it --save
# md样式
cnpm install github-markdown-css
views/BlogDetail.vue
<template>
<div>
<Header></Header>
<div class="mblog">
<h2> {{ blog.title }}</h2>
<el-link icon="el-icon-edit" v-if="ownBlog">
<router-link :to="{name: 'BlogEdit', params: {blogId: blog.id}}" >
编辑
</router-link>
</el-link>
<el-divider></el-divider>
<div class="markdown-body" v-html="blog.content"></div>
</div>
</div>
</template>
<script>
import 'github-markdown-css'
import Header from "../components/Header";
export default {
name: "BlogDetail.vue",
components: {Header},
data() {
return {
blog: {
id: "",
title: "",
content: ""
},
ownBlog: false
}
},
created() {
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
this.$axios.get('/blog/' + blogId).then(res => {
const blog = res.data.data
_this.blog.id = blog.id
_this.blog.title = blog.title
var MardownIt = require("markdown-it")
var md = new MardownIt()
var result = md.render(blog.content)
_this.blog.content = result
_this.ownBlog = (blog.userId === _this.$store.getters.getUser.id)
})
}
}
</script>
<style scoped>
.mblog {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
width: 100%;
min-height: 700px;
padding: 20px 15px;
}
</style>
import router from "./router";
// 路由判断登录 根据路由配置文件的参数
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)) { // 判断该路由是否需要登录权限
const token = localStorage.getItem("token")
console.log("------------" + token)
if (token) { // 判断当前的token是否存在 ; 登录存入的token
if (to.path === '/login') {
} else {
next()
}
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
至此,前端页面也已经搭建完成,启动测试一下。
这个项目只是一个练手前后端分离的小demo,并不算的上什么真正意义上的项目。找的是一位博主的项目练手,他的文档链接放在这里:https://my.oschina.net/u/3080373/blog/4294136?_from=gitee_search
因为是刚学完springboot、vue,已经可以开发一个ssm项目,然后来体验一下前后端分离项目,而这个项目可以体验到搭建前后端分离框架。后端主要是ssm架构,spring+springmvc+mybatis,在这里是用spirngboot项目框架整合起来,然后用的是mybatis-plus这个很方便的工具,安全权限用的是shiro,会话缓存在redis,用户认证用的jwt,大概的技术就是这些。