项目是前后端分离的一个项目,文章总体分为java后端接口和vue前端页面。
编译器使用的IDEA2021,一开始先创建新的项目骨架,直接选择Spring Initializr创建。数据层一开始学习springboot的时候用的mybatis,易上手也很方便,但是编写SQL语句时工作量很大,尤其是字段多、关联表多时,更是如此,所以用的mybatis-plus(https://baomidou.com/guide/),为简化而生只…,启动自动注入基本CRUD。性能基本无损耗,直接面向对象操作,还有分页插件等等。然后同时权限也是需要注意的,所以用了Shiro的配置,直接使用,节约时间。其次考虑项目可能要部署多台所以一些会话信息等需要共享,然后使用了redis,因为是前后端分离,因此使用jwt作为用户身份验证,简介完开始搭建项目脚手架。
用IDEA开发项目,创建项目步骤比较简单就直接跳过了。
开发工具与环境:
pom的依赖引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
devtools
:项目加热部署lombok
:简化代码的工具例如用@Data
注解省略getter setter ToString等Mybatis-Puls官方文档
第一步:引入mybastis-puls依赖、以及模板引擎依赖后续还有使用代码生成
<!--mp-->
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mp代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
第二步:写yml配置文件
# DataSource Config
server:
port: 8082
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
这里配置了基本的接口、数据库信息以及还要配置mybatis-plus的mapper的xml文件的扫描路径
第三步:开启mapper接口扫描,添加分页插件
新建一个包:通过@mapperScan
注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。PaginationInterceptor
是一个分页插件。
@Configuration
@EnableTransactionManagement
@MapperScan("com.vueblog.mapper")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
@MapperScan
注解
之前是,直接在Mapper类上面添加注解@Mapper
,这种方式要求每一个mapper类都需要添加此注解,麻烦,通过使用@MapperScan
可以指定要扫描的Mapper类的包的路径
@EnableTransactionManagement
@EnableTransactionManagement表示开启事务支持,在springboot项目中一般配置在启动类上,效果等同于xml配置的@Transactional
便可。
第四步:代码生成
官方文档有提供代码生成器,直接使用即可,写上了自己的参数后,就可以根据数据表信息生成entity层、service层、mapper层等
大致生成如下:
第五步:测试
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
public Object test(@PathVariable("id") Long id) {
return userService.getById(id);
}
}
目前的前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式有利于前后端的交互与UI的展示。
创建一个Rusult的类,用来做异步统一返回的将结果封装主要是几点
也可以是通过一般形式
@Data
public class Result implements Serializable {
private String code;
private String msg;
private Object data;
public static Result succ(Object data) {
Result m = new Result();
m.setCode("0");
m.setData(data);
m.setMsg("操作成功");
return m;
}
public static Result succ(String mess, Object data) {
Result m = new Result();
m.setCode("0");
m.setData(data);
m.setMsg(mess);
return m;
}
public static Result fail(String mess) {
Result m = new Result();
m.setCode("-1");
m.setData(null);
m.setMsg(mess);
return m;
}
public static Result fail(String mess, Object data) {
Result m = new Result();
m.setCode("-1");
m.setData(data);
m.setMsg(mess);
return m;
}
}
Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多,1.Shiro官方文档2.shiro官方教程里面有教程如何上手shiro
JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,用于在各方之间以JSON对象安全传输信息,这些信息可以通过数字签名进行验证和信任。 可以使用RSA的公钥/私钥对对JWT进行签名。这里是通过采用token或者jwt作为跨域身份验证解决方案,来在登录环节进行用户身份验证,以获取资源。是目前最流行的跨域认证解决方案。
JwtFilter
。SecurityManager
进行一个登录,登录完成就能识别了当前用户是谁。json
数据给前端。@RequiresRoles("admin")
注解就是需要admin用户权限才可以访问该接口。第一步:接下来先引入shiro-redis依赖、jwt依赖、还有hutool工具包
先简介一下hutool:
hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,Hutool是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。
以计算MD5为例:
【以前】打开搜索引擎 -> 搜“Java MD5加密” -> 打开某篇博客-> 复制粘贴 -> 改改好用
【现在】引入Hutool -> SecureUtil.md5()
Hutool的存在就是为了减少代码搜索成本,避免网络上参差不齐的代码出现导致的bug。
<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的工作流程:
Shiro进行认证的本质还是通过过滤器进行拦截,过滤器拦截后判断是否需要进行认证,如果需要,取出”token”并交给SecurityManager
进行认证,认证通过后放行,如果不需要认证则直接放行。
为什么token打上引号,是因为token并不一定是普遍意义上的JWT(json web token),也可以是基于BASIC HTTP的token,还可以是表单中的用户名和密码。
所以Shiro中就内置了一些常用的Filter,比如内置的AuthenticatingFilter
类,有基于表单认证的FormAuthenticationFilter
类和基于BASIC HTTP的BasicHttpAuthenticationFilter
类,还有不需要认证直接放行的AnonymousFilter
类,也有一些用于检查Roles
和Permissions
的过滤器,具体的可以去DefaultFilter
中看看。
/**
* shiro启用注解拦截控制器
*/
@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);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(redisCacheManager);
/*
* 关闭shiro自带的session,详情见文档
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
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;
}
// 开启注解代理(默认好像已经开启,可以不要)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
return creator;
}
}
上面ShiroConfig,我们主要做了几件事情:
引入RedisSessionDAO
和RedisCacheManager
,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
重写了SessionManager
和DefaultWebSecurityManager
,同时在DefaultWebSecurityManager
中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
在ShiroFilterChainDefinition
中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication
,这样控制权限访问。
AccountRealm:
AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
JwtToken jwt = (JwtToken) token;
log.info("jwt----------------->{}", jwt);
String userId = jwtUtils.getClaimByToken((String) jwt.getPrincipal()).getSubject();
User user = userService.getById(Long.parseLong(userId));
if(user == null) {
throw new UnknownAccountException("账户不存在!");
}
if(user.getStatus() == -1) {
throw new LockedAccountException("账户已被锁定!");
}
AccountProfile profile = new AccountProfile();
BeanUtil.copyProperties(user, profile);
log.info("profile----------------->{}", profile.toString());
return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
}
}
其实主要就是doGetAuthenticationInfo登录认证这个方法,可以看到我们通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro。
接下来我们逐步分析里面出现的新类:
1、shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,这里是我们自定义一个JwtToken,来完成shiro的supports方法。
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;
}
}
@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
...
}
// 获取jwt的信息
public Claims getClaimByToken(String token) {
...
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
@Data
public class AccountProfile implements Serializable {
private Long id;
private String username;
private String avatar;
}
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
markerhub:
jwt:
# 加密秘钥
secret: f4e2e52034348f86b67cde581c0f9eb5
# token有效时长,7天,单位秒
expire: 604800
header: token
前面引入了spring-boot-devtools热部署,需要添加一个配置文件,在resources目录下新建文件夹META-INF,然后新建文件spring-devtools.properties,这样热重启时候才不会报错。
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
AuthenticationFilter类
和AuthenticatingFilter
类
注意这两个类名的不同。
AuthenticationFilter
只实现了isAccessAllowed(...)
方法:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 返回绑定到当前线程上的主体是否已经认证的结果
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
而AuthenticatingFilter
继承自AuthenticationFilter类
:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = 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);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
这个isAccessAllowed(…)方法:如果已经认证则返回true,或者请求不需要认证并且设置了”permissive”,设置”permissive”是什么意思呢:比如说配置过滤器的路由策略时:map.add(“/**”,”authc[permissive]”)
AuthenticatingFilter
类@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取 token
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 token = request.getHeader("Authorization");
if(StringUtils.isEmpty(token)) {
return true;
} else {
// 判断是否已过期
Claims claim = jwtUtils.getClaimByToken(token);
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 httpResponse = (HttpServletResponse) response;
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result r = Result.fail(throwable.getMessage());
String json = JSONUtil.toJsonStr(r);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
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);
}
}
一般我们自定义过滤器就是继承AuthenticatingFilter
类,然后只需要重写onAccessDenied(...)
方法,当头部没有Authorization
时候,我们直接通过,不需要自动登录;当带有的时候,首先我们校验jwt的有效性在其中调用executeLogin
方法进行认证,然后提供创建Token的具体逻辑也就是createToken(..)
方法就可以了。
如果有特殊需求,比如对于Option请求直接放行,那么可以重写isAccessAllowed(...)
方法,判断是否是option请求,一般来说我们还是会调用一下父类的通用判断方法:super.isAccessAllowed(...)
onLoginFailure
:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
preHandle
:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
处理办法如下:通过使用@ControllerAdvice
来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)
来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。
@ControllerAdvice
表示定义全局控制器异常处理
@ExceptionHandler
表示针对性异常处理,可对每种异常针对性处理。
主要捕获以下几个异常:
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExcepitonHandler {
// 捕捉shiro的异常
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public Result handle401(ShiroException e) {
return Result.fail(401, e.getMessage(), null);
}
/**
* 处理Assert的异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) throws IOException {
log.error("Assert异常:-------------->{}",e.getMessage());
return Result.fail(e.getMessage());
}
/**
* @Validated 校验错误异常处理
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) throws IOException {
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 = RuntimeException.class)
public Result handler(RuntimeException e) throws IOException {
log.error("运行时异常:-------------->",e);
return Result.fail(e.getMessage());
}
}
postman测试shiro异常:
在未使用@RequiresAuthentication
注解:
使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。就是需要认证才能访问。
使用@RequiresAuthentication
注解:
测试成功
springboot框架集成了Hibernate validatior
因此直接使用
第一步:首先在实体的属性上添加对应的校验规则,比如:
@TableName("m_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@NotBlank(message = "昵称不能为空")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
...
}
第二步 :这里我们使用@Validated注解方式,如果实体不符合要求,系统会抛出异常,那么我们的异常处理中就捕获到MethodArgumentNotValidException
。
/**
* 测试实体校验
* @param user
* @return
*/
@PostMapping("/save")
public Object testUser(@Validated @RequestBody User user) {
return user.toString();
}
/**
* 解决跨域问题
*/
@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("*");
}
}
通过使用账户密码,把用户的id生成jwt,返回给前端。
@RestController
public class AccountController {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
/**
* 默认账号密码:Gliogz / 111111
*
*/
@CrossOrigin
@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()
);
}
// 退出
@GetMapping("/logout")
@RequiresAuthentication
public Result logout() {
SecurityUtils.getSubject().logout();
return Result.succ(null);
}
}
开始业务接口,完成博客列表和博客详情页接口开发
@RestController
public class BlogController {
@Autowired
BlogService blogService;
@GetMapping("/blogs")
public Result blogs(Integer currentPage) {
if(currentPage == null || currentPage < 1) currentPage = 1;
Page page = new Page(currentPage, 5)
IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));
return Result.succ(pageData);
}
@GetMapping("/blog/{id}")
public Result detail(@PathVariable(name = "id") Long id) {
Blog blog = blogService.getById(id);
Assert.notNull(blog, "该博客已删除!");
return Result.succ(blog);
}
@RequiresAuthentication
@PostMapping("/blog/edit")
public Result edit(@Validated @RequestBody Blog blog) {
System.out.println(blog.toString());
Blog temp = null;
if(blog.getId() != null) {
temp = blogService.getById(blog.getId());
Assert.isTrue(temp.getUserId() == ShiroUtil.getProfile().getId(), "没有权限编辑");
} 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.succ("操作成功", null);
}
}
接下来完成vueblog前端的部分功能。
用到以下技术:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import BlogDetail from '../views/BlogDetail.vue'
import BlogEdit from '../views/BlogEdit.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
redirect: { name: 'Blogs' }
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/blogs',
name: 'Blogs',
// 懒加载
component: () => import('../views/Blogs.vue')
},
{
path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
name: 'BlogAdd',
meta: {
requireAuth: true
},
component: BlogEdit
},
{
path: '/blog/:blogId',
name: 'BlogDetail',
component: BlogDetail
},
{
path: '/blog/:blogId/edit',
name: 'BlogEdit',
meta: {
requireAuth: true
},
component: BlogEdit
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
<template>
<div>
<el-container>
<el-header>
<img src="https://api.freelogodesign.org/files/d50c0f3f9bde4137a5d6708500a652a2/thumb/logo_200x200.png?v=0"
style="height: 300%; margin-top: -60px; width:350px">
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.vue",
data() {
return {
ruleForm: {
username: 'Gilogz',
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("http://localhost:8082/login", this.ruleForm).then(res => {
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")
})
}
});
},
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;
}
.demo-ruleForm{
max-width: 500px;
margin: 0 auto;
}
style>
<template>
<div class="m-content">
<h3>Gilogz的博客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/0/88/03b0d39583f48206768a7534e55bcpng.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>
<template>
<div>
<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>
<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>
<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>