Author: Lu Yiming

Follow the public number: MarkerHub, reply [vueblog], get the source code of the project!

Video of the project: www.bilibili.com/video/BV1PQ…

Presentation of the project: www.markerhub.com:8084/blogs

Reprint please keep this quote, thank you!

Follow the public number: MarkerHub, reply [vueblog], get the source code of the project!

Follow the public number: MarkerHub, reply [vueblog], get the source code of the project!

Separate items at the front and back ends

The article is generally divided into two parts, Java back-end interface and VUE front-end page, relatively long, because do not want to be released separately, really want you to learn in 4 hours, haha.

First look at the effect:

Presentation of the project: www.markerhub.com:8084/blogs

So let’s start typing code.

Java back-end interface development

1, the preface

To build a project skeleton from scratch, it is best to choose appropriate and familiar technology, which is easy to expand in the future and suitable for micro-service system. Therefore, it is necessary to use Springboot as the base of our framework.

Then the data layer, we commonly used is Mybatis, easy to start, convenient maintenance. Mybatis Plus (mp.baomidou.com/) is used to simplify the development of a single table, but it is difficult to operate a single table, especially when adding or reducing fields. CRUD operations, thus saving significant time.

As a project skeleton, permissions are also something we can’t ignore. Shiro is easy to configure and easy to use, so we use Shiro as our permissions.

Considering that the project may need to deploy multiple units, at this time, our session and other information need to be shared, Redis is now the mainstream cache middleware, which is also suitable for our project.

Then because of the separation of the front and back ends, we use JWT as our user credentials.

Ok, let’s start building the scaffolding for our project!

Technology stack:

  • SpringBoot
  • mybatis plus
  • shiro
  • lombok
  • redis
  • hibernate validatior
  • jwt

Map: www.markerhub.com/map/131

2. Create a Springboot project

Here, we use IDEA to develop our project. The new step is relatively simple, so we will not take screenshots.

Development Tools and environment:

  • idea
  • mysql
  • jdk 8
  • maven3.3.9

The structure of the new project is as follows. The SpringBoot version uses the latest 2.2.6.RELEASE

The POM JAR package is imported as follows:

<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>
Copy the code
  • Devtools: A hot-load restart plug-in for a project
  • Lombok: A tool for simplifying code

3. Integrate Mybatis Plus

Next, let’s integrate Mybatis Plus, so that the project can complete the basic operation of adding, deleting, modifying and checking. Steps is very simple: can visit website: mp.baomidou.com/guide/insta…

Step 1: Import the JAR package

The JAR package for Mybatis Plus is imported into the POM. Since code generation will be involved later, we also need to import the page template engine. In this case, freemarker is used.

<! --mp--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> < version > 3.2.0 < / version > < / dependency > < the 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> <! <groupId>com.baomidou</groupId> <artifactId> myBatis -plus-generator</artifactId> The < version > 3.2.0 < / version > < / dependency >Copy the code

Step 2: Then write the configuration file

# DataSource Config 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: admin mybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xmlCopy the code

In addition to configuring the database information, the scan path of myabtis Plus mapper XML file is also configured. Don’t forget this step. Step 3: Enable mapper interface scanning and add paging plug-ins

Create a new package: the @MapPERScan annotation specifies the package in which the interface is to become an implementation class, and all interfaces under the package are compiled to generate the corresponding implementation class. PaginationInterceptor is a pagination plugin.

  • com.markerhub.config.MybatisPlusConfig
@Configuration @EnableTransactionManagement @MapperScan("com.markerhub.mapper") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); return paginationInterceptor; }}Copy the code

Step 4: Code generation

If you don’t have any other plugins, you are ready to use MyBatis Plus. You can use mybatis Plus to generate an entity, Service, mapper, and other interface and implementation classes based on the database table information.

  • com.markerhub.CodeGenerator

Because the code is relatively long, I will not post it, in the code warehouse to see ha!

Select * from user where user = ‘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 DEFAULT CHARSET=utf8;
CREATE TABLE `m_blog` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `title` varchar(255) NOT NULL,
  `description` varchar(255) NOT NULL,
  `content` longtext,
  `created` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  `status` tinyint(4) DEFAULT NULL.PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;
INSERT INTO `vueblog`.`m_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`) VALUES ('1'.'markerhub'.'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg'.NULL.'96e79218965eb72c92a549dd5a330112'.'0'.'the 2020-04-20 10:44:01'.NULL);
Copy the code

Run the main method of CodeGenerator and enter the table name: m_user to generate the following result:

Get:

Concise! Convenient! After the above steps, we have basically integrated the MyBatis Plus framework into the project.

Ps: Well, notice that the m_blog table code is also generated.

Write a test in UserController:

@RestController @RequestMapping("/user") public class UserController { @Autowired UserService userService; @GetMapping("/{id}") public Object test(@PathVariable("id") Long id) { return userService.getById(id); }}Copy the code

Access:http://localhost:8080/user/1The results are as follows: integration is successful!

3. Unified result packaging

Here we use a Result class, which is used for our asynchronous uniform return Result encapsulation. In general, several elements are necessary for the outcome

  • Success can be indicated by code (for example, 200 indicates success and 400 indicates exception).
  • Result message
  • The resulting data

So the encapsulation is as follows:

  • com.markerhub.common.lang.Result
@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 (" Operation succeeded "); 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; }}Copy the code

4. Integrate Shiro + JWT and share session

We generally use Redis to store shiro’s cache and session information. Therefore, we need to integrate not only Shiro but also Redis. In the open source project, we found a starter that can quickly integrate Shiro – Redis with simple configuration, and it is recommended to use here.

Since what we need to do is to separate the skeleton of the project from the front end, we generally use token or JWT as the cross-domain authentication solution. Therefore, in the process of integrating Shiro, we need to introduce JWT authentication process.

So let’s start integrating:

We use a Shro-redis-spring-boot-starter JAR package, see the official documentation: github.com/alexxiyang/…

Step 1: Import the Shiro – Redis Starter package: also the JWT kit, and to simplify development, I introduced the HuTool kit.

<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis-spring-boot-starter</artifactId> The < version > 3.2.1 < / version > < / dependency > <! Hutool -all</artifactId> <version>5.3.3</version> </dependency> <! <groupId> <artifactId> JJWT </artifactId> <version>0.9.1< version> </dependency>Copy the code

Step 2: Write the configuration:

ShiroConfig

  • com.markerhub.config.ShiroConfig
@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); /* * Close shiro's session. For details, see the document */ 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"); / / annotations mainly by checking permissions 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; } // Enable the annotation proxy (it seems to be enabled by default, Can don't @ 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; }}Copy the code

Above ShiroConfig, we mainly do a few things:

  1. RedisSessionDAO and RedisCacheManager are introduced to solve the problem that shiro’s permission data and session information can be saved to Redis to realize session sharing.
  2. Rewrote our SessionManager and DefaultWebSecurityManager, at the same time, in order to close the shiro in DefaultWebSecurityManager own session, we need to set to false, Users will no longer be able to log in to Shiro in session mode. Login with JWT credentials will follow.
  3. In ShiroFilterChainDefinition, we no longer through the coding form intercept Controller access path, but all routing need after JwtFilter this filter, and then determine whether a request header contains information on JWT, have and then login, not just skip. After skipping, there are shiro annotations in the Controller that intercept again, such as @requiresAuthentication, to control access.

So, next, let’s talk about AccountRealm, which appears in ShiroConfig, and JwtFilter.

AccountRealm

AccountRealm is shiro’s logon or permission verification logic. This is the core of shiro’s logon or permission verification logic

  • Supports: In order for realm to support JWT credential validation
  • DoGetAuthorizationInfo: permission verification
  • DoGetAuthenticationInfo: indicates login authentication verification

Let’s take a look at the AccountRealm code in general and then analyze it one by one:

  • com.markerhub.shiro.AccountRealm
@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(" Account does not exist! ") ); } if(user.getStatus() == -1) {throw new LockedAccountException(" Account locked! ") ); } AccountProfile profile = new AccountProfile(); BeanUtil.copyProperties(user, profile); log.info("profile----------------->{}", profile.toString()); return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName()); }}Copy the code

In fact, the main method is doGetAuthenticationInfo login authentication. You can see that we get the user information through JWT, judge the status of the user, and finally throw the corresponding exception information. If not, it is encapsulated as SimpleAuthenticationInfo and returned to Shiro. Next, we will analyze the new classes that appear in it step by step:

Shiro supports UsernamePasswordToken by default. We now use the JWT method, so we define a JwtToken to complete Shiro’s supports method.

JwtToken

  • com.markerhub.shiro.JwtToken
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; }}Copy the code

JwtUtils is a utility class that generates and verifies JWT. Some of the key information related to JWT is configured from the project configuration file:

@Component @ConfigurationProperties(prefix = "markerhub.jwt") public class JwtUtils { private String secret; private long expire; private String header; /** * generate JWT token */ public String generateToken(long userId) {... } public Claims getClaimByToken(String Token) {... } /** * Whether the token is expired * @return true: */ public Boolean isTokenExpired(Date expiration) {return expiration. Before (new Date()); }}Copy the code

In AccountRealm we also use AccountProfile, which is a carrier of user information returned after a successful login.

AccountProfile

  • com.markerhub.shiro.AccountProfile
@Data
public class AccountProfile implements Serializable {
    private Long id;
    private String username;
    private String avatar;
}
Copy the code

Step 3, OK, after the basic validation route is complete, we need to configure a few basic information:

Shiro -redis: enabled: true redis-manager: host: 127.0.0.1:6379 markerHub: JWT: # secret: F4e2e52034348f86b67cde581c0f9eb5 # token the effective time, 7 days, the unit seconds expire: 604800 header: tokenCopy the code

Step 4: In addition, if your project uses spring-boot-devtools, you need to add a configuration file. In the resources directory, create a new folder, meta-INF, and create a new file, spring-devtools.properties.

  • resources/META-INF/spring-devtools.properties
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
Copy the code

JwtFilter

Step 5: Define JWT filter JwtFilter.

This filter is our focus, here we are inherited Shiro built-in AuthenticatingFilter, one can be built into the automatic login method of filter, some classmate inheritance BasicHttpAuthenticationFilter can too.

We need to override several methods:

  1. CreateToken: To implement login, we need to generate our custom supported JwtToken
  2. OnAccessDenied: intercept verification, when there is no Authorization in the header, we pass directly, no automatic login is required; When with, we first verify the validity of JWT, and if so, we go straight to the executeLogin method for automatic login
  3. OnLoginFailure: the method used when logging in to an exception, we simply wrap the exception information and throw it
  4. PreHandle: Pre-intercept for interceptors. Since we are a front and back end analysis project, in addition to cross-domain global configuration, we also need cross-domain support in interceptors. This way, interceptors are not restricted before they enter the Controller.

Let’s look at the overall code:

  • com.markerhub.shiro.JwtFilter
@Component public class JwtFilter extends AuthenticatingFilter { @Autowired JwtUtils jwtUtils; @Override protected AuthenticationToken createToken(ServletRequest servletRequest, Throws Exception {// Obtain 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 {// Claim = jwtutils.getClaimByToken (token);} else {// Claim = jwtutils.getClaimByToken (token); If (claim = = null | | jwtUtils. IsTokenExpired (claim) getExpiration ())) {throw new ExpiredCredentialsException (" token has failed, Please log in again!" ); } // Return executeLogin(servletRequest, servletResponse); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; Try {// Throwable Throwable = equetetcause () == null? e : e.getCause(); Result r = Result.fail(throwable.getMessage()); String json = JSONUtil.toJsonStr(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * cross-domain support */ @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")); // When you cross domains, you first send an OPTIONS request. Here we give the OPTIONS request directly to return to normal state if (it) getMethod () equals (RequestMethod. OPTIONS. The name ())) { httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value()); return false; } return super.preHandle(request, response); }}Copy the code

So at this point, our Shiro is integrated and using JWT for identity verification.

5. Exception handling

If the exception handling mechanism is not configured, tomcat or Nginx 5XX page will be returned by default, which is not very friendly to ordinary users and users do not understand what the situation is. It is up to our programmers to return a friendly and simple format to the front end.

The solution is as follows: @ExceptionHandler(value = runtimeException.class) specifies the types of exceptions to be caught. The handling of this Exception is global. This is where all the exceptions like this are handled.

  • com.markerhub.common.exception.GlobalExceptionHandler

Step 2. Define global exception handling. @ControllerAdvice defines global controller exception handling, and @ExceptionHandler specifies specific exception handling.

/** * Global Exception Handling */ @slf4@RestControllerAdvice public class GlobalExcepitonHandler {// Catch Shiro exception @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(ShiroException.class) public Result handle401(ShiroException e) { return Result.fail(401, e.getMessage(), null); } / Assert of exception handling * / * * * @ ResponseStatus (HttpStatus. BAD_REQUEST) @ ExceptionHandler (value = IllegalArgumentException. Class)  public Result handler(IllegalArgumentException e) throws IOException { The log. The error (" Assert exception: -- -- -- -- -- -- -- -- -- -- -- -- -- - > {} ", um participant etMessage ()); return Result.fail(e.getMessage()); } /** * @responseStatus (httpStatus.bad_request) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) throws IOException { Log.error (" Runtime exception :-------------->",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(" RuntimeException :-------------->",e); return Result.fail(e.getMessage()); }}Copy the code

Above we catch a few exceptions:

  • ShiroException: An exception thrown by Shiro, such as no permission, user login exception
  • IllegalArgumentException: Handles Assert exceptions
  • MethodArgumentNotValidException: physical check exception handling
  • RuntimeException: Catch other exceptions

6. Entity verification

When we submit the form data, we can use javascript plug-ins such as jQuery Validate and Hibernate Validatior to verify the form data.

We use the SpringBoot framework as the base, so Hibernate Validatior is already automatically integrated.

So what does it look like?

Step 1: First add the corresponding validation rules to the attributes of the entity, such as:

@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 = "username ") private String username; @notblank (message = "@blank ") @email (message =" @blank ") private String Email; . }Copy the code

The second step: here we use the @ Validated annotations way, if the entity does not conform to the requirements, the system will throw an exception, then we can capture MethodArgumentNotValidException in exception handling.

  • com.markerhub.controller.UserController
/** * @param user * @return */ @postmapping ("/save") public Object testUser(@validated @requestBody user user) { return user.toString(); }Copy the code

7. Cross-domain issues

Since the analysis is in front and back end, cross-domain problems cannot be avoided. We directly conduct global cross-domain processing in the background:

  • com.markerhub.config.CorsConfig
/** * Implements WebMvcConfigurer */ @Configuration public class implements WebMvcConfigurer {@override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600) .allowedHeaders("*"); }}Copy the code

Ok, because the interface for our system is relatively simple, SO I won’t integrate Swagger2, which is simple. Let’s jump right into the business of writing the login interface.

8. Login interface development

The logon logic is very simple, we just need to accept the account password, and then generate the user ID JWT, and return it to the preceding paragraph, so we put JWT on the header in order to delay the subsequent JWT. The specific code is as follows:

  • com.markerhub.controller.AccountController
@RestController public class AccountController { @Autowired JwtUtils jwtUtils; @Autowired UserService userService; /** * Default password: markerhub / 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, "user does not exist "); if(! User.getpassword ().equals(secureutil.md5 (logindto.getPassword ()))) {return result.fail (" Password error! ") ); } 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", "username"). user.getUsername()) .put("avatar", user.getAvatar()) .put("email", user.getEmail()) .map() ); } // Exit @getMapping ("/logout") @requiresauthentication public Result logout() {Securityutalls.getSubject ().logout(); return Result.succ(null); }}Copy the code

Interface test:

9. Blog interface development

Now that our skeleton is complete, we can add our business interface. Let’s take a simple list of blogs and blog details page as an example.

  • com.markerhub.controller.BlogController
@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, "This blog has been deleted!" ); 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 (), "No permission to edit "); } 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(" operation succeeded ", null); }}Copy the code

Note that @requiresAuthentication specifies interfaces that require login to access. Other interfaces that require permissions can be annotated by Shiro. The interface is relatively simple, we do not say much, the basic increase, delete and check. Note that the Edit method is a restricted resource that requires a login to operate.

Interface test:

10. Back-end summary

All right, it’s a bit of a rush to get a basic skeleton in one article, but the basics are here. Then we will develop our front-end interface.

Video of the project: www.bilibili.com/video/BV1PQ…

Vue front-end page development

1, the preface

Next, let’s complete some of the functionality of the vueBlog front end. The following techniques may be used:

  • vue
  • element-ui
  • axios
  • mavon-editor
  • markdown-it
  • github-markdown-css

This project practice needs a little bit of vUE foundation, hope you have some understanding of vUE instructions, so that we explain up more simple ha.

2. Project demonstration

Let’s first take a look at what the project we need to complete looks like. Considering that many students don’t have enough knowledge of the style, I try to use the native component style of element-UI to complete the interface of the whole blog. Without further ado, go straight to the picture above:

Online experience: Markerhub.com :8083

3. Environment preparation

Great oaks from little acorns, we next step by step to complete, first of all we install vue environment, MY practice environment is Windows 10 ha.

First, we go to the node.js website (nodejs.org/zh-cn/), download the latest long-term version, and run it directly. After the installation is complete, we have the node and NPM environment.

Check the version information after the installation is complete:

2. Next, we install the vue environment

CNPM # installation taobao NPM NPM install - g - registry=https://registry.npm.taobao.org # vue - cli installation depend on package CNPM vue install - g - cliCopy the code

4. Create a new project

Open vue's visual management tool vue UICopy the code

Above we installed Taobao NPM respectively, CNPM is to improve the speed of our installation dependency. Vue UI is a visual project management tool for @vue/ CLI3.0. You can run projects, package projects, check projects, etc. For beginners, you can memorize fewer commands, haha. Create the vueblog-vue project

Running vue UI will bring us a page called http://localhost:8080:

Then switch to Create and make sure the directory is at the same level as when you are running vue UI. This is easy to manage and switch. Then click the button [Create new Envy here]

In the next step, enter the project name “vueblog-vue” in the project folder, do not change the other items, click next, select [Manual], then click next, click the button as shown in the figure, select The route by Router, state management Vuex, and remove js verification.

In the next step, also select “Use History Mode for Router”, click “Create Project”, and then select “Create Project, do not save the preset” in the popup window to enter the project creation.

After a few moments, the project is initialized. In the steps above, we created a vue project and installed Router and Vuex. So we can use it later.

Let’s take a look at the overall vueblog-Vue project structure

│ ├── Build Script Directory │ ├── build-server.js Run the local build server, │ ├── dev-client.js Hot reload script for the development server │ ├─ Web-pack.base.conf. Js │ ├─ Web-pack.base.conf. Js │ ├─ Web-pack.base.conf Webpack.dev.conf. Js Wabpack development Environment │ ├── Webpack.prod.conf. Js Wabpack Production Environment Configuration │ ├── Config Project Configuration │ ├── dev Index.js Project configuration file │ ├── prod.env.js Production environment variable │ ── test.env.js Test environment variable │ ── mock Mock data directory │ ─ hello.js ├─ package.json The NPM package configuration file, which defines the NPM script for the project, Rely on information such as package ├ ─ ─ the SRC source directory │ ├ ─ ─ the main, js entrance js file │ ├ ─ ─ app. Vue root component │ ├ ─ ─ components common components directory │ │ └ ─ ─ the title. The vue │ ├ ─ ─ assets resources directory, │ ├── images │ ├─ logo.png │ ├── routes │ ├─ index.js │ ├── store-level data │ ├─ ch.htm Index.js │ ├── views │ ├─ ├── hello.vue │ ├─ notfound.vue │ ├─ static └ ─ ─ the test test file directory (2 e) unit&e └ ─ ─ unit unit test ├ ─ ─ index. The js script entrance ├ ─ ─ karma. Conf., js karma configuration file └ ─ ─ specs sheet test case directory └ ─ ─ Hello.spec.jsCopy the code

Install element-UI

Next we introduce the Element-UI component (Element.eleme.cn) so that we can get a nice vUE component and develop a nice blog interface.

The command is simple:

# Install element-ui CNPM install Element-ui --saveCopy the code

Then we open main.js in the project SRC directory and introduce the elemental-UI dependency.

import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)
Copy the code

Then we can have the pleasure of selecting components on the website and copying the code to use directly in our project.

6. Install axiOS

Next, let’s install Axios (www.axios-js.com/), which is a PROMISe-based HTTP library, so we can use this tool to improve our development efficiency when we do front and back interconnections.

Install command:

cnpm install axios --save
Copy the code

Then we also introduce axios globally in main.js.

import axios from 'axios'
Vue.prototype.$axios = axios //
Copy the code

$axios.get() component to initiate our request.

7. Page routing

Next, let’s define the route and the page first. Since we are only doing a simple blog project with few pages, we can define the route first and then develop slowly, so that we can use the link directly where we need to use:

We define several pages under the Views folder:

  • Blogdetail.vue (Blog details page)
  • Blogedit.vue (Edit blog)
  • Blogs. Vue
  • Login.vue (Login page)

Then configure the routing center:

  • router\index.js
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', // lazy load component: () = > import ('.. / views/Blogs. Vue ')}, {path: '/ blog/add', / / attention on path: '/ blog / : blogId' before 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 routerCopy the code

Next we go to develop our page. Meta: requireAuth: true indicates a restricted resource that requires a login word to access, which we will use later in route permission interception.

8. Login page

Next, let’s make a login page. The form components are found directly on the Element-UI website. The login page is relatively simple with two input fields and a submit button. Emmm, I just post the code

  • views/Login.vue
<template>
  <div>
    <el-container>
      <el-header>
        <router-link to="/blogs">
        <img src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png"
             style="height: 60%; margin-top: 10px;">
        </router-link>
      </el-header>
      <el-main>
        <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px"
                 class="demo-ruleForm">
          <el-form-item label="User name" prop="username">
            <el-input type="text" maxlength="12" v-model="ruleForm.username"></el-input>
          </el-form-item>
          <el-form-item label="Password" prop="password">
            <el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm('ruleForm')">The login</el-button>
            <el-button @click="resetForm('ruleForm')">reset</el-button>
          </el-form-item>
        </el-form>
      </el-main>
    </el-container>
  </div>
</template>
<script>
  export default {
    name: 'Login'.data() {
      var validatePass = (rule, value, callback) = > {
        if (value === ' ') {
          callback(new Error('Please enter your password'));
        } else{ callback(); }};return {
        ruleForm: {
          password: '111111'.username: 'markerhub'
        },
        rules: {
          password: [{validator: validatePass, trigger: 'blur'}].username: [{required: true.message: 'Please enter a user name'.trigger: 'blur'},
            {min: 3.max: 12.message: '3 to 12 characters long'.trigger: 'blur'}}}; },methods: {
      submitForm(formName) {
        const _this = this
        this.$refs[formName].validate((valid) = > {
          if (valid) {
            // Submit logic
            this.$axios.post('http://localhost:8081/login'.this.ruleForm).then((res) = >{
              const token = res.headers['authorization']
              _this.$store.commit('SET_TOKEN', token)
              _this.$store.commit('SET_USERINFO', res.data.data)
              _this.$router.push("/blogs")})}else {
            console.log('error submit!! ');
            return false; }}); },resetForm(formName) {
        this.$refs[formName].resetFields(); }},mounted() {
      this.$notify({
        title: 'Look here:'.message: 'Follow the public account: MarkerHub, reply to [vueblog], get the project information and source code'.duration: 1500}); }}</script>
Copy the code

I can’t find a good way to explain it, so I’ll just post the code and explain it later. In the above code, there are actually two main things being done

1. Form verification

2. Click the login button to log in the event

Let’s look at the element UI component and analyze the code that initiates the login:

const token = res.headers['authorization']
_this.$store.commit('SET_TOKEN', token)
_this.$store.commit('SET_USERINFO', res.data.data)
_this.$router.push("/blogs")
Copy the code

The token information is obtained from the returned result request header, and the status of the token and user information is submitted using the Store. Once that’s done, we adjust to the/Blogs route, which is the blog list page.

The token status is synchronized

So in store/index.js, the code looks like this:

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_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) => {
      localStorage.setItem("token", '')
      sessionStorage.setItem("userInfo", JSON.stringify(''))
      state.userInfo = {}
    }
  },
  getters: {
    getUser: state => {
      return state.userInfo
    }
  },
  actions: {},
  modules: {}
})
Copy the code

We use localStorage to store tokens, and we use sessionStorage to store user information. After all, we do not need to keep the user information for a long time. After saving the token information, we can initialize the user information at any time. Of course, since this project is relatively simple, considering beginners, I did not do many relatively complex packaging and functions, of course, after learning this project, I want to go further, and I can learn and transform by myself.

Define the global AXIOS interceptor

Click the login button to initiate the login request, when the success of the data returned, if the password is wrong, we should not also pop-up message prompt. In order to make this error popover work everywhere, I made a post interceptor to AXIos, which tells me if the resulting code or status is not normal when returning data.

Create a file in the SRC directory axios.js (the same class as main.js) and define the interceptors for Axios:

import axios from 'axios' import Element from "element-ui"; import store from "./store"; import router from "./router"; = 'http://localhost:8081' axios. Defaults. BaseURL axios. Interceptors. Request. Use (config = > {the console. The log (" front ") / / Request headers can be set unified return config}) axios. Interceptors. Response. Use (response = > {const res = response. The data; Console. log(" post intercept ") if (res.code === 200) {return response} else {// Pop-up exception Message Element.Message({ message: response.data.msg, type: 'error', duration: 2 * 1000}) return promise.reject (Response.data.msg)}}, error => { console.log('err' + error)// for debug if(error.response.data) { error.message = error.response.data.msg } // If (error.response.status === 401) {store.com MIT ('REMOVE_INFO'); router.push({ path: '/login' }); Message = 'Please log in again '; } if (error-.response.status === 403) {error-. message = 'not authorized enough to access '; } Element.Message({ message: error.message, type: 'error', duration: 3 * 1000 }) return Promise.reject(error) })Copy the code

Pre-interception, in fact, can be unified for all requests that require permission to assemble the header token information, this does not need to be used in the configuration, my small project is relatively small, so, it is not needed ~ ~

Then import axios.js into main.js

Import './axios.js' // Request interceptionCopy the code

The entity returned by the backend is Result, code is 200 when SUCC, and 400 when fail, so we can judge whether the Result is normal according to here. In addition, when the permission is insufficient, the status code of the request result can be used to judge whether the result is normal. So we’ve done a simple thing here.

When the login fails, the effect is as follows:

9. Blog list

After the login is complete, go directly to the blog list page, and then load the data of the blog list to render. At the same time, we need to display the user information in the page header, because this module is used in many places, so we extract the user information in the page header as a separate component.

Header user information

So, let’s complete the user information header, which should contain three parts of information: ID, avatar, and user name, and this information is already in the sessionStorage after we log in. Therefore, we can get user information through the store’s getters.

This doesn’t look too complicated, so let’s post the code:

  • components\Header.vue
<template> <div class="m-content"> <h3> Welcome to MarkerHub </h3> <div class="block"> <el-avatar :size="50" :src="user.avatar"></el-avatar> <div>{{ user.username }}</div> </div> <div class="maction"> <el-link <span style =" divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider: divider" href="/blog/add" :disabled="! Divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider = divider> <span v-show="hasLogin"> <span v-show="hasLogin"> <span v-show="hasLogin"> <span v-show="hasLogin"> <span v-show=" danger" > < span> </div> </div> </template> <script> export default {name: "Header", data() {return {hasLogin: false, user: {username: 'login first ', avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" }, blogs: {}, currentPage: 1, total: 0 } }, methods: { logout() { const _this = this this.$axios.get('http://localhost:8081/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>Copy the code

The created() code above initializes the user’s information, controls the login and exit buttons through the hasLogin state, and disables the post link, so that the user’s information can be displayed. And then there’s the exit button, and then there’s the logout() method in the methods, and the logic is simple, just go to /logout, because we already set the baseURL of the axios request in axios.js, so we don’t need the link prefix anymore. Since it is a restricted resource that can only be accessed after login, Authorization is included in the header. The user information and token information in the store are clear. The login page is displayed.

Then pages that need header user information need only a few steps:

import Header from "@/components/Header"; Data () {components: {Header}} #Copy the code

The blog page

The next thing is the list page, and we need to do paging, and the list we use the timeline component directly in the Element-UI as our list style, and it looks pretty good. And then there’s our paging component.

Several parts of information are required:

  • The paging information
  • The content of the blog list, including ID, title, summary, and creation time
  • views\Blogs.vue
<template> <div class="m-container"> <Header></Header> <div class="block"> <el-timeline> <el-timeline-item v-bind: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> </div> <el-pagination class="mpage" background layout="prev, pager, next" :current-page=currentPage :page-size=pageSize @current-change=page :total="total"> </el-pagination> </div> </template> <script> import Header from "@/components/Header"; export default { name: "Blogs", components: {Header}, data() { return { blogs: {}, currentPage: 1, total: 0, pageSize: 5 } }, methods: { page(currentPage) { const _this = this this.$axios.get('http://localhost:8081/blogs? currentPage=' + currentPage).then((res) => { console.log(res.data.data.records) _this.blogs = res.data.data.records _this.currentPage = res.data.data.current _this.total = res.data.data.total _this.pageSize = res.data.data.size }) } }, mounted () { this.page(1); } } </script>Copy the code

Data () directly defines blogs, a list of blogs, as well as some paging information. Methods () defines the paging interface page (currentPage). The parameter is the page number currentPage that needs to be adjusted. After getting the result, directly assign the value. Mounted () then calls this.page(1) directly from the mounted() method. Perfect. Using the Element-UI component is easy and fast! Notice in the title here we added the link, using the tag.

10. Blog Editor (publish)

Let’s click on the post blog link to adjust to the /blog/add page, here we need to use a Markdown editor, in the vue component, the more useful is mavon-editor, so let’s use it directly. First install the mavon-Editor components:

Install mavon – editor

Vue based MarkDown editor Mavon-Editor

cnpm install mavon-editor --save
Copy the code

Then register globally in main.js:

// Register globally
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)
Copy the code

Ok, so let’s define our blog form:

<template> <div class="m-container"> <Header></Header> <div class="m-content"> <el-form ref="editForm" status-icon :model="editForm" :rules="rules" label-width="80px"> <el-form-item label=" title" prop="title"> <el-input V-model ="editForm.title"></el-input> </el-form-item> <el-form-item label=" abstract "prop="description"> <el-input Type ="textarea" v-model=" editform. description"></el-input> </el-form-item> <el-form-item label=" content" prop="content"> <mavon-editor v-model="editForm.content"/> </el-form-item> <el-form-item> <el-button type="primary" @ click = "submitForm ()" > create immediately < / el - button > < el - button > cancel < / el - button > < / el - form - item > < / el - form > < / div > < / div > < / template > <script> import Header from "@/components/Header"; export default { name: "BlogEdit", components: {Header}, data() { return { editForm: { id: null, title: ', description: ', content: '}, rules: {title: [{required: true, message: 'Please enter the title ', trigger: 'blur'}, {min: 3, Max: 50, message: '3 to 50 characters long ', trigger: 'blur'}], description: [{required: true, message:' Please enter a summary ', trigger: 'blur'} ] } } }, created() { const blogId = this.$route.params.blogId const _this = this if(blogId) { this.$axios.get('/blog/' + blogId).then((res) => { const blog = res.data.data _this.editForm.id = blog.id _this.editForm.title = blog.title _this.editForm.description = blog.description _this.editForm.content = blog.content }); } }, methods: { submitForm() { const _this = this this.$refs.editForm.validate((valid) => { if (valid) { this.$axios.post('/blog/edit', this.editForm, { headers: { "Authorization": }}). Then ((res) => {_this.$alert(' operation successful ', 'info ', {confirmButtonText:' confirm ', callback: action => { _this.$router.push("/blogs") } }); }); } else { console.log('error submit!! '); return false; } }) } } } </script>Copy the code

The logic is still simple, verify the form, then click the button to submit the form, notice that the Authorization information is added to the header, return a result pop-up indicating success, and jump to the blog list page. Emm, it’s no different than writing Ajax. Familiarize yourself with some of vUE’s instructions. Then, because edit and add are on the same page, you have the create() method, such as getting the ID with the blogId 7 from the edit link /blog/7/edit. The blog information is displayed. Const blogId = this.$route.params.blogid.

By the way, mavon-editor is registered globally, so we can use the components directly:

<mavon-editor v-model="editForm.content"/>
Copy the code

The effect is as follows:

11. Blog details

In this case, we use a plugin called Markdown-it to parse the MD document and import it into github- Markdown-c. So called MD style.

Here’s how:

CNPM install github-markdown- CSSCopy the code

Then you can use it where you want to render:

  • views\BlogDetail.vue
<template> <div class="m-container"> <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: Id}}"> </ divider></ divider>< div class="content markdown-body" v-html="blog.content"></div> </div> </div> </template> <script> import 'github-markdown-css/github-markdown.css' // Then add the style markdown-body import Header from "@/components/Header"; export default { name: "BlogDetail", components: { Header }, data() { return { blog: { userId: null, title: "", description: "", content: "" }, ownBlog: false } }, methods: { getBlog() { const blogId = this.$route.params.blogId const _this = this this.$axios.get('/blog/' + blogId).then((res) => { console.log(res) console.log(res.data.data) _this.blog = res.data.data var MarkdownIt = require('markdown-it'), md = new MarkdownIt(); var result = md.render(_this.blog.content); _this.blog.content = result // Determine if it is your own article, OwnBlog = (_this.blog.userid === _this.$store.getters.getuser.id)}); } }, created() { this.getBlog() } } </script>Copy the code

The logic is pretty simple. The create() method calls getBlog() to request the blog detail interface, and the returned blog detail content is rendered by the Markdown-it tool.

Import styles again:

import 'github-markdown.css'
Copy the code

Then add a class of Markdown-body to the content div. The effect is as follows:

A small edit button has been added under the heading to determine whether the ownBlog (which determines if the author of the blog is the same person as the login user) is displayed.

12. Route permission interception

If you are not logged in, you will be redirected to the login page. Therefore, we will define a js file in the SRC directory:

  • src\permission.js
import router from "./router"; // Route judgment Log in according to the route configuration file parameter route.beforeeach ((to, from, Next) => {if (to.matched. Some (record => record.meta. RequireAuth)) {// Check whether the route requires login permission const token = Localstorage.getitem ("token") console.log("------------" + token) if (token) {// Check whether the current token exists; If (to.path === '/login') {} else {next()}} else {next({path: '/login'})}} else {next()}})Copy the code

RequireAuth: true is required for login. So here we check the token status beforeEach route (router.beforeeach) to see if the token needs to be redirected to the login page.

Name: 'BlogAdd', meta: {requireAuth: true}, Component: BlogEdit}Copy the code

We then import our permission-js in main.js

Import './ permission-js' // Route interceptionCopy the code

13. Front-end summary

Ok, basically all the pages have been developed, I did not post the CSS style information, you directly on Github clone down to check.

Project Summary

Ok, the project first here, spent 3 and a half days to record a set of corresponding video, remember to watch, give me three consecutive wow.

Video of the project: www.bilibili.com/video/BV1PQ…

Follow the public number: MarkerHub, reply [vueblog], get the source code of the project!

Follow the public number: MarkerHub, reply [vueblog], get the source code of the project!

Follow the public number: MarkerHub, reply [vueblog], get the source code of the project!