Java back end interface and Vue front end
1. See the effect first
publish an article
Background login
Blog details
Java back end interface development:
Using springboot+mybaitsPlus+shiro+jwt+lombok+redis; database mysql5.7+
Here, we use IDEA to develop the background interface. It's easy to create a new interface
2. Create the shpringboot project
Project structure:
pom.xml
<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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency>
3. Integrating MyBtisPlus
1. Introduce related dependency
<!--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 Code generator --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.2.0</version> </dependency>
2. Write configuration file
#port server: port: 8081 #Database related 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: 123456 mybatis-plus: type-aliases-package: com.hu.my_web_site.entity mapper-locations: classpath*:/mapper/**.xml
3. Write GenerateCode to generate code automatically
// For example, execute the main method, the console input module table name, and press enter to automatically generate the corresponding project directory public class CodeGenerator { /** * <p> * Read console content * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("Please enter" + tip + ": "); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("Please enter the correct" + tip + "!"); } public static void main(String[] args) { // Code generator AutoGenerator mpg = new AutoGenerator(); // Global configuration GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); // gc.setOutputDir("D:\\test"); gc.setAuthor("hucong"); gc.setOpen(false); // gc.setSwagger2(true); entity attribute Swagger2 annotation gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); // Data source configuration DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); mpg.setDataSource(dsc); // Package configuration PackageConfig pc = new PackageConfig(); pc.setModuleName(null); pc.setParent("com.hu.my_web_site"); mpg.setPackageInfo(pc); // Custom configuration InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // If the template engine is freemarker String templatePath = "/templates/mapper.xml.ftl"; // If the template engine is velocity // String templatePath = "/templates/mapper.xml.vm"; // Custom output configuration List<FileOutConfig> focList = new ArrayList<>(); // Custom configuration will be output first focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // Customize the output file name. If you set the prefix and suffix for Entity, please note that the name of xml will change accordingly!! return projectPath + "/src/main/resources/mapper/" + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // Configure templates TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // Policy configuration 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("Table name, multiple comma separated").split(",")); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix("m_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }
4. Database related sql. Here are two tables. Log in to the registry and save the Blog article table
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) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, `created` datetime(0) NOT NULL ON UPDATE CURRENT_TIMESTAMP(0), `status` tinyint(4) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; CREATE TABLE `m_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `status` int(5) NOT NULL, `created` datetime(0) NULL DEFAULT NULL, `last_login` datetime(0) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `UK_USERNAME`(`username`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `m_user` VALUES (1, 'hucong', ''http://www.16sucai.com/2016/05/82507.html'', '1069997178@qq.com', '96e79218965eb72c92a549dd5a330112', 0, '2020-04-20 10:44:01', NULL);
After construction, the structure of the project is as shown in the figure above
Test interface (that is to test whether it is connected to the database) I will not write, certainly right!!! ha-ha
mybatisplus integration completed
4. Unified result encapsulation
Result class, which is used to encapsulate the results returned asynchronously and uniformly. Generally speaking, there are several essential elements in the result (encapsulated in the common package)
Whether it is successful or not can be represented by code (for example, 200 indicates success, 400 indicates exception)
Result message
Result data
@Data public class Result implements Serializable { private int code; // 200 is normal, non 200 indicates abnormal private String msg; private Object data; public static Result succ(Object data) { return succ(200, "Operation successful", 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; } public static Result fail(String msg) { return fail(400, msg, null); } 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; } }
5. Integrate shiro+jwt to realize session sharing
Basic ideas:
1 * *. Import pom dependency**
<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis-spring-boot-starter</artifactId> <version>3.2.1</version> </dependency> <!-- hutool Tools--> <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>
2. Write yml
#shiro-redis shiro-redis: enabled: true redis-manager: host: 127.0.0.1:6379
3. Write the configuration class ShiroConfig (put it under the config package)
@Configuration public class ShiroConfig { @Autowired JwtFilter jwtFilter; @Bean public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); // inject redisSessionDAO 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; } /** * Define filter chain * @return */ @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; } }
1. Introduce RedisSessionDAO and RedisCacheManager to solve the problem that the permission data and session information of shiro can be saved in redis to realize session sharing.
2. The session manager and DefaultWebSecurityManager are rewritten. At the same time, in order to turn off the session mode of shiro, it needs to be set to false in DefaultWebSecurityManager, so that users can no longer log in to shiro through the session mode. You'll log in with jwt credentials later.
3. In ShiroFilterChainDefinition, instead of blocking the Controller access path by encoding, all routes need to pass through the JwtFilter filter, and then judge whether the request header contains jwt information. Log in if you have, or skip if you don't. After skipping, there is a shiro annotation in the Controller to intercept again, such as @ requireauthentication, to control permission access. So, next, account Realm (custom Realm) appears in ShiroConfig, and JwtFilter (filter).
4.AccountRealm (under shiro package)
AccountRealm is the logic of shiro's login or permission verification, which is the core,
Three methods need to be overridden, namely
supports: in order for the realm to support jwt's credential verification
doGetAuthorizationInfo: permission verification
doGetAuthenticationInfo: login authentication verification
@Component public class AccountRealm extends AuthorizingRealm { @Autowired JwtUtils jwtUtils; @Autowired UserService userService; /** * In order to make the realm support jwt's certificate verification * @param token * @return */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * to grant authorization * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } /** * authentication * @param token * @return * @throws AuthenticationException */ @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("Account does not exist"); } if (user.getStatus() == -1) { throw new LockedAccountException("Account locked"); } AccountProfile profile = new AccountProfile(); BeanUtil.copyProperties(user, profile); return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName()); } }
In fact, it's mainly the method of doGetAuthenticationInfo login authentication. You can see that we get the user information through jwt, judge the user's status, and finally throw the corresponding exception information. Otherwise, we encapsulate it as SimpleAuthenticationInfo and return it to shiro.
Next, we will gradually analyze the new classes: 1. The default support of shiro is UsernamePasswordToken, and now we use the jwt method, so here we customize a JwtToken to complete the support method of shiro.
5.JwtToken (under shiro package)
1.jwttken:
public class JwtToken implements AuthenticationToken { //shiro's default support is UsernamePasswordToken, // Now we use the jwt method, so here we customize a JwtToken, // To complete shiro's supports method private String token; public JwtToken(String jwt) { this.token = jwt; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
2. jwt utils is a tool class for generating and verifying jwt. Some jwt related key information is configured from the project configuration file: (under the utils package)
/** * jwt Tools */ @Slf4j @Data @Component @ConfigurationProperties(prefix = "hu.jwt") public class JwtUtils { private String secret; private long expire; private String header; /** * Generate jwt token */ public String generateToken(long userId) { Date nowDate = new Date(); //Expiration time 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 Expired or not * @return true: be overdue */ public boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); } }
3. In AccountRealm, we also use AccountProfile, which is a carrier of user information returned after successful login,
@Data public class AccountProfile implements Serializable { //AccountProfile is a carrier of user information returned after successful login, private Long id; private String username; private String avatar; private String email; }
ok, after the basic verification route is completed, a small amount of basic information configuration is required:
hu: jwt: # secret: f4e2e52034348f86b67cde581c0f9eb5 # token expire: 604800 header: Authorization
6.JwtFilter (under shiro package)
This filter is our focus. Here, we inherit Shiro's built-in AuthenticatingFilter, a filter that can be built-in to automatically log in methods, and also inherit BasicHttpAuthenticationFilter.
Several methods need to be overridden:
createToken: to implement login, I need to generate jwttoken supported by my custom
Access denied: intercept verification. When the header does not have Authorization, it passes directly and does not need to log in automatically. When it does, first verify the validity of jwt, and execute the executeLogin method directly to realize automatic login if there is no problem
onLoginFailure: the method entered when logging in an exception, encapsulating the exception information directly and throwing
preHandle: the pre interception of interceptors, because this is a front-end and back-end separation project. In addition to cross domain global configuration, cross domain support is also required in interceptors. In this way, the interceptor will not be restricted before entering the Controller.
@Component public class JwtFilter extends AuthenticatingFilter { @Autowired JwtUtils jwtUtils; //To log in, we need to generate our own JwtToken @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = request.getHeader("Authorization"); if(StringUtils.isEmpty(jwt)) { return null; } return new JwtToken(jwt); } /** * Intercept verification: when there is no Authorization in the header, we directly pass it without automatic login; * When there is, first of all, we verify the effectiveness of jwt. If there is no problem, we will directly execute the executeLogin method to realize automatic login * @param servletRequest * @param servletResponse * @return * @throws Exception */ @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 { // Verify jwt Claims claim = jwtUtils.getClaimByToken(jwt); if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) { throw new ExpiredCredentialsException("token Invalid, please login again"); } // Perform login return executeLogin(servletRequest, servletResponse); } } /** * When logging in an exception, we directly encapsulate the exception information and throw * @param token * @param e * @param request * @param response * @return */ @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; } /** * The pre interception of interceptors is because we are a front-end and back-end analysis project. In addition to cross domain global configuration, the, * We also need to provide cross domain support in interceptors. In this way, the interceptor will not be restricted before entering the Controller. * @param request * @param response * @return * @throws Exception */ @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 cross domain, an OPTIONS request will be sent first. Here, we directly return the OPTIONS request to the normal state if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
shiro has been integrated and used jwt for authentication.
6 global exception handling
Location: (package com.hu.my_web_site.common.exception ;)
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * @ControllerAdvice Represents the definition of global controller exception handling, * @ExceptionHandler Indicates targeted exception handling. Each exception can be targeted. */ /** * // Catch shiro's exception * @param e * @return */ @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(value = ShiroException.class) public Result handler(ShiroException e) { log.error("Runtime exception:----------------{}", e); return Result.fail(401, e.getMessage(), null); } /** * @Validated Check exception error * @param e * @return */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) { log.error("Entity verification exception:----------------{}", e); BindingResult bindingResult = e.getBindingResult(); ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); return Result.fail(objectError.getDefaultMessage()); } /** * Handle the exception of Assert * @param e * @return */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public Result handler(IllegalArgumentException e) { log.error("Assert Exception:----------------{}", e); return Result.fail(e.getMessage()); } /** * RuntimeException: Catch other exceptions * @param e * @return */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = RuntimeException.class) public Result handler(RuntimeException e) { log.error("Runtime exception:----------------{}", e); return Result.fail(e.getMessage()); } }
7 verification entity class
user entity class
@TableId(value = "id", type = IdType.AUTO) private Long id; @NotBlank( message="Nickname cannot be empty") private String username; private String avatar; @NotBlank(message = "Mailbox cannot be empty") @Email(message = "Incorrect mailbox format") private String email;
blog entity class
//Inspection Time @JsonFormat(pattern = "yyy-MM-dd") private LocalDateTime created;
8. cross domain problems
1. It's very simple. It only needs to be on the controller
Add @ CrossOrigin
2. You can also write global cross domain in config package (I don't like to write so many codes, so I used the last annotation method)
/** * Solve cross domain problems */ @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("*"); } }
9. Login interface development
@RestController @CrossOrigin public class loginController { @Autowired UserService userService; @Autowired JwtUtils jwtUtils; @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, "The user name does not exist or the password is wrong"); if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){ return Result.fail("Incorrect password"); } 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); } }
10. Development of blog interface
@RestController @CrossOrigin public class BlogController { @Autowired private BlogService blogService; //paging operation @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); } //Query a record by id @GetMapping("/blog/{id}") public Result detail(@PathVariable Long id){ Blog blog = blogService.getById(id); //Judge whether it is empty Assert.notNull(blog,"The content is empty"); return Result.succ(blog); } //Modify or add @PutMapping("/blog/edit") public Result edit(@Validated @RequestBody Blog blog){ Blog temp; if (blog.getId() != null){ //edit //Get blog details through id temp= blogService.getById(blog.getId()); // You can only edit your own articles System.out.println(((AccountProfile) SecurityUtils.getSubject().getPrincipal()).getId()); Assert.isTrue(temp.getUserId().longValue() == ((AccountProfile) SecurityUtils.getSubject().getPrincipal()).getId(), "No permission to edit"); }else{ //add to temp = new Blog(); temp.setUserId(((AccountProfile) SecurityUtils.getSubject().getPrincipal()).getId()); temp.setCreated(LocalDateTime.now()); temp.setStatus(0); } BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status"); blogService.saveOrUpdate(temp); return Result.succ(null); } //Delete a record by id @GetMapping("/delete/{id}") private Result delete(@PathVariable("id") Long id){ boolean remove = blogService.removeById(id); Assert.notNull(remove,"Delete succeeded"); return Result.succ(remove); } }
Background interface complete