This is the 27th day of my participation in the August Genwen Challenge.More challenges in August

Exit data return

Jwt-username token – random code – redis JwtLogoutSuccessHandler

// Add Spring annotations
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
    // Inject JWT utility classes
    @Autowired
    JwtUtils jwtUtils;
    @Override
    public void onLogoutSuccess(HttpServletRequest Request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// Exit if it is null
        if(authentication ! =null) {
            new SecurityContextLogoutHandler().logout(Request,response,authentication);
        }
        // Format of the response
        response.setContentType("application/json; charset=UTF-8");
/ / the output stream
        ServletOutputStream outputStream = response.getOutputStream();
/ / headers
        response.setHeader(jwtUtils.getHeader(), "");
// The result returned

        Results result = Results.succ("");

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); }}Copy the code

No permission data is returned


@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        // Set the header encoding specification
        httpServletResponse.setContentType("application/json; charset=UTF-8");
        // Give an underprivileged state
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        // Set the output stream
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        // Set error exception output
        Results fail = Results.fail(e.getMessage());
        // Write the format
        outputStream.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
        // Refresh to close the streamoutputStream.flush(); outputStream.close(); }}Copy the code

SpringSecurity is perfectly integrated into our project.

Resolve cross-domain problems

We use Postman for the above debugging. If we connect with the front end, there will be a cross-domain problem of CorsConfig

@Configuration
public class CorsConfig implements WebMvcConfigurer {

	private CorsConfiguration buildConfig(a) {
		CorsConfiguration corsConfiguration = new CorsConfiguration();
		corsConfiguration.addAllowedOrigin("*");
		corsConfiguration.addAllowedHeader("*");
		corsConfiguration.addAllowedMethod("*");
		corsConfiguration.addExposedHeader("Authorization");
		return corsConfiguration;
	}

	@Bean
	public CorsFilter corsFilter(a) {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/ * *", buildConfig());
		return new CorsFilter(source);
	}

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/ * *")
				.allowedOrigins("*")
// .allowCredentials(true)
				.allowedMethods("GET"."POST"."DELETE"."PUT")
				.maxAge(3600); }}Copy the code

Menu interface development

Development menu interface, because these three tables: user table, role table, menu table, menu table, menu table is not need to obtain information through other tables. For example, users need to associate roles, and roles need to associate menus, while menus do not need to actively associate other tables. The link to get menu navigation and permissions is /sys/menu/nav, and the JSON data for our menu navigation should look like this:

{   title:
 'Role Management',  
  icon: 'el-icon-rank',  
   path: '/sys/roles',  
    name: 'SysRoles',
       component: 'sys/Role',  
        children: []}
Copy the code

The returned permission data should then be an array:

["sys:menu:list"."sys:menu:save"."sys:user:list". ]Copy the code

Notice that in the navigation menu there is a children, a submenu, which is a tree, because our menu might look like this:

System Management - Menu Management - Add menusCopy the code

This is a level 3 menu. Notice how this relationship relates. Our SysMenu entity class has parentId, but no children, so we can add children to SysMenu, but we can not add children, because we also need a DTO, so that we can return the json data format above. Add a children: SysMenu


@Data
@EqualsAndHashCode(callSuper = true)
public class SysMenu extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /** * parent menu ID, level 1 menu is 0 */
    @notnull (message = "Parent menu cannot be empty ")
    private Long parentId;

    @notblank (message = "Menu name cannot be empty ")
    private String name;

    /** * menu URL */
    private String path;

    /** * Authorization (multiple users are separated by commas, such as user:list,user:create) */
    @notblank (message = "Menu authorization code cannot be blank ")
    private String perms;

    private String component;

    /**
     * 类型     0:目录   1:菜单   2:按钮
     */
    @notnull (message = "Menu type cannot be empty ")
    private Integer type;

    /** * menu icon */
    private String icon;

    /** ** sort */
    @TableField("orderNum")
    private Integer orderNum;

    @TableField(exist = false)
    private List<SysMenu> children = new ArrayList<>();
}

Copy the code

SysMenuDto, knowing what data to return, we just need to fill in the data, right

package com.example.demo.dto;

import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
public class SysMenuDto implements Serializable {
    private Long id;
    private String name;
    private String title;
    private String icon;
    private String path;
    private String component;
    private List<SysMenuDto> children = new ArrayList<>();
}

Copy the code

SysMenuController

package com.example.demo.controller;


import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.Result.Results;
import com.example.demo.dto.SysMenuDto;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUser;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.List;

/** ** <p> * Front-end controller * </p> **@author fjj
 * @sinceThe 2021-07-05 * /
@RestController
@RequestMapping("/sys/menu")
public class SysMenuController extends BaseController {
    // Get the link to the menu
    @GetMapping("/nav")
    // Get nav navigation
    public Results nav(Principal principal) {
        // Get the current login user
        SysUser byUsername = sysUserService.getByUsername(principal.getName());
        // Access permission information is separated by commas
        String userAuthorityInfo = sysUserService.getUserAuthorityInfo(byUsername.getId());
// Convert to an array
        String[] strings = StringUtils.tokenizeToStringArray(userAuthorityInfo, ",");
        // Get navigation information
        List<SysMenuDto> navs = sysMenuService.getCurrentUserNav();
        // Return the result
        return Results.succ(MapUtil.builder()
                .put("authoritys", strings)
                .put("nav", navs)
                .build()
        );
    }

    // Get user information
    @GetMapping("/userInfo")
    public Results userInfo(Principal principal) {
        SysUser sysUser = sysUserService.getByUsername(principal.getName());
        return Results.succ(MapUtil.builder()
                .put("id", sysUser.getId())
                .put("username", sysUser.getUsername())
                .put("avatar", sysUser.getAvatar())
                .put("created", sysUser.getCreated())
                .map()
        );
    }
    @GetMapping("/info/{id}")
    @PreAuthorize("hasAuthority('sys:menu:list')")
    public Results info (@PathVariable(name = "id") Long id) {
        return Results.succ(sysMenuService.getById(id));
    }
    @GetMapping("/list")
    @PreAuthorize("hasAuthority('sys:menu:list')")
    public Results list (a) {
      List<SysMenu> menus =  sysMenuService.tree ();
      return Results.succ(menus);
    }
    / / save
    @PostMapping("/save")
    @PreAuthorize("hasAuthority('sys:menu:save')")
    public Results save (@Validated @RequestBody SysMenu sysMenu) {
       sysMenu.setCreated(LocalDateTime.now());
       sysMenuService.save(sysMenu);
       return Results.succ(sysMenu);
    }
    / / update
    @PostMapping("/update")
    @PreAuthorize("hasAuthority('sys:menu:update')")
    public Results update (@Validated @RequestBody SysMenu sysMenu) {
        sysMenu.setUpdated(LocalDateTime.now());
        sysMenuService.updateById(sysMenu);
        // Since this is an update operation, we need to be aware of the cache
        sysUserService.clearUserAuthorityInfoByMenuId(sysMenu.getId());
        return Results.succ(sysMenu);
    }
/ / delete
  @PostMapping("/delete/{id}")
  @PreAuthorize("hasAuthority('sys:menu:delete')")
    public Results delete (@PathVariable ("id") Long id) {
        // Check whether the child node still exists and delete it if it does not
      int parent_id = sysMenuService.count(new QueryWrapper<SysMenu>().eq("parent_id", id));
      if (parent_id > 0) {
          return Results.fail("Please delete the submenu first.");
      }
      // Clear all caches
      sysUserService.clearUserAuthorityInfoByMenuId(id);
      sysMenuService.removeById(id);
      // Delete the associated table data
      sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("menu_id",id));
      return Results.succ("success"); }}Copy the code

The Principal Principal method is used to inject information about the current user. The getName method is used to obtain the current user name. SysUserService. GetUserAuthorityInfo methods we’ve said before, when we login to complete or authentication needs to return when user permissions. And then through the StringUtils. TokenizeToStringArray the string array form separated by a comma. Focus on and sysMenuService getcurrentUserNav, access to the current user menu navigation, SysMenuServiceImpl

package com.example.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dto.SysMenuDto;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysUser;
import com.example.demo.mapper.SysMenuMapper;
import com.example.demo.mapper.SysUserMapper;
import com.example.demo.service.ISysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/** * <p> * Service implementation class * </p> **@author fjj
 * @sinceThe 2021-07-05 * /
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper.SysMenu> implements ISysMenuService {
    @Autowired
    SysUserServiceImpl sysUserService;
    @Autowired
    SysUserMapper sysUserMapper;

    @Override
    public List<SysMenuDto> getCurrentUserNav(a) {
        // Get the login user
        String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        // Obtain the value by user name
        SysUser sysUser = sysUserService.getByUsername(username);
        // Get the menu Id by user Id
        List<Long> navMenuIds = sysUserMapper.getNavMenuIds(sysUser.getId());
        // Get the menu from the menu ID
        List<SysMenu> sysMenus = this.listByIds(navMenuIds);
        // Convert to a tree stump structure
        List<SysMenu> menustree = buildTreeMeun(sysMenus);
        // Entity class conversion
        return convert(menustree);
    }

    @Override
    public List<SysMenu> tree(a) {
        // Get all menu information
        List<SysMenu> sysMenus = this.list(new QueryWrapper<SysMenu>().orderByAsc("orderNum"));
        // Convert to a tree stump structure
        List<SysMenu> menus = buildTreeMeun(sysMenus);
        return menus;
    }

    private List<SysMenuDto> convert(List<SysMenu> menustree) {
        ArrayList<SysMenuDto> menuDtos = new ArrayList<>();
        menustree.forEach(m -> {
            SysMenuDto dto = new SysMenuDto();
            dto.setId(m.getId());
            dto.setName(m.getPerms());
            dto.setTitle(m.getName());
            dto.setComponent(m.getComponent());
            dto.setPath(m.getPath());
            // If the length is greater than that, the bytes are copied
            if (m.getChildren().size() > 0) {
                // The child node calls the current method to replicate.
                dto.setChildren(convert(m.getChildren()));
            }
            menuDtos.add(dto);
        });
        return menuDtos;
    }
// Convert the stump structure
    private List<SysMenu> buildTreeMeun(List<SysMenu> sysMenus) {
        // Prepare the returned list
        ArrayList<SysMenu> finalMenus = new ArrayList<>();
        // Find the respective child nodes
        for (SysMenu sysMenu : sysMenus) {
            for (SysMenu sysMenu1 : sysMenus) {
            // If the id is the same, the child is your own
                if(sysMenu.getId() == sysMenu1.getParentId()) { sysMenu.getChildren().add(sysMenu1); }}// Extract the parent node
            if (sysMenu.getParentId() == 0L) { finalMenus.add(sysMenu); }}returnfinalMenus; }}Copy the code

Interface sysUserMapper. GetNavMenuIds we’ve written before, by user id for the id of the menu, and then is transformed into a tree structure, behind buildTreeMenu method of thinking is very simple, our reality the menu cycle, let all the child nodes of the menu to find their first, Then we get the top menu out, so there are two levels below the top, and two levels have their own three levels. Finally convert converts menu to menuDto. This one is easy, I won’t say it. Ok, navigation menu has been developed, let’s write the menu management add, delete, change and check, because the menu list is also a tree interface, this time we will not get the current user’s menu list, but all the menus and then form a tree structure, the same idea, different data. Delete, update the menu when remember to call the menu ID clear user permissions cache information method ha. Then each method is preceded by a permission annotation: @preauthorize (“hasAuthority(‘sys:menu:delete’)”), this requires users to have specific operation permissions to call this interface, sys:menu:delete These data are not written in random, we must be consistent with the database data. The Component field also communicates with the front end, because this is the component page linked to the front end. With add, delete, change and check, we go to add all our menu permission data first. The effect is as follows:

Role interface development

Add, delete, change and check the role is also simple, and so few fields SysRoleController

package com.example.demo.controller;


import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Result.Results;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysRole;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUserRole;
import com.example.demo.utils.Const;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/** ** <p> * Front-end controller * </p> **@author fjj
 * @sinceThe 2021-07-05 * /
@RestController
@RequestMapping("/sys/role")
public class SysRoleController extends BaseController {
    @PreAuthorize("hasAuthority('sys:role:list')")
    @GetMapping("/info/{id}")
    public Results info(@PathVariable("id") Long id) {
        // Get the entity class
        SysRole byId = sysRoleService.getById(id);
        // Get the meunid of the associated table
        List<SysRoleMenu> role_id = sysRoleMenuService.list(new QueryWrapper<SysRoleMenu>().eq("role_id", byId));
        // Get the meunid from the stream
        List<Long> collect = role_id.stream().map(p -> p.getMenuId()).collect(Collectors.toList());
        byId.setMenuIds(collect);
        return Results.succ(byId);
    }
    @PreAuthorize("hasAuthority('sys:role:list')")
    @GetMapping("/list")
    public Results list(String name) {
        // check whether there is a name
        Page page = sysRoleService.page(getPage(),
                new QueryWrapper<SysRole>().like(StrUtil.isNotBlank(name), "name", name));
        return Results.succ(page);
    }
    @PreAuthorize("hasAuthority('sys:role:save')")
    @PostMapping("/save")
    public Results save(@Validated @RequestBody SysRole sysRole) {
        // Add a method
        // Get the time of the current change
        sysRole.setCreated(LocalDateTime.now());
        // The saved state
        sysRole.setStatu(Const.STATUS_ON);
        sysRoleService.save(sysRole);
        return Results.succ(sysRole);
    }
    @PreAuthorize("hasAuthority('sys:role:update')")
    / / update
    @PostMapping("/update")
    public Results update(@Validated @RequestBody SysRole sysRole) {
        // Set the update time
        sysRole.setCreated(LocalDateTime.now());
        // Call the updated method
        sysRoleService.updateById(sysRole);
        // Delete the cache
        sysUserService.clearUserAuthorityInfoByRoleId(sysRole.getId());
        return Results.succ(sysRole);
    }
// Batch delete
@PreAuthorize("hasAuthority('sys:role:delete')")
    @PostMapping("/delete")
    // Add transaction to avoid deletion failure
    @Transactional
    public Results delete(@RequestBody Long[] RoleIds) {
        // Call the method
        sysRoleService.removeByIds(Arrays.asList(RoleIds));
        // Drop the intermediate table
        sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().in("role_id",RoleIds));
        sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("role_id",RoleIds));
        // Delete the cache
        Arrays.stream(RoleIds).forEach(f -> {
            sysUserService.clearUserAuthorityInfoByRoleId(f);
        });
        return Results.succ("success");
    }
    @PreAuthorize("hasAuthority('sys:role:perm')")
    @PostMapping("/perm/{roleId}")
    @Transactional
    public Results info(@PathVariable("roleId") Long roleId,@RequestBody Long[] menuId) {
        // The set to store
       List<SysRoleMenu> sysRoleMenus = new ArrayList<>();
       Arrays.stream(menuId).forEach(menuid -> {
           SysRoleMenu roleMenu = new SysRoleMenu();
           roleMenu.setMenuId(menuid);
           roleMenu.setRoleId(roleId);
           sysRoleMenus.add(roleMenu);
       });
       // Delete the record
        sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("role_id",roleId));
        // Save the new one
        sysRoleMenuService.saveBatch(sysRoleMenus);
        // Delete the cache
        sysUserService.clearUserAuthorityInfoByRoleId(roleId);
        returnResults.succ(menuId); }}Copy the code

In the above method: info method is used to obtain role information. This method is not only used when editing roles, but also used when displaying role association menus. Therefore, we need to query the IDS of all menus associated with roles, that is, the operation of assigning permissions. Corresponding to the front end is like this. Click Assign Permission, all menu lists will pop up, and then select the menu that has been associated according to the ID of the menu that has been associated with the role.

User interface development

User management there is a user associated role role assignment operation, and role associated menu writing similar

package com.example.demo.controller;


import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Result.Results;
import com.example.demo.dto.PassDto;
import com.example.demo.entity.SysRole;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUser;
import com.example.demo.entity.SysUserRole;
import com.example.demo.utils.Const;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** ** <p> * Front-end controller * </p> **@author fjj
 * @sinceThe 2021-07-05 * /
@RestController
@RequestMapping("/sys/user")
public class SysUserController extends BaseController {
    // Inject encrypted
    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;
    @GetMapping("/info/{id}")
    @PreAuthorize("hasAuthority('sys:user:list')")
    public Results info(@PathVariable("id") Long id) {
        SysUser sysUser = sysUserService.getById(id);
        // assert whether or not it is null
        Assert.notNull(sysUser, "The administrator could not be found");
        List<SysRole> sysRoles = sysRoleService.listRolesByUserId(id);
        sysUser.setSysRoles(sysRoles);
        return Results.succ(sysUser);
    }
    @PreAuthorize("hasAuthority('sys:user:list')")
    @GetMapping("/list")
    public Results list(String username) {
        Page<SysUser> page = sysUserService.page(getPage(), new QueryWrapper<SysUser>().like(StringUtils.isNotBlank(username), "username", username));
        page.getRecords().forEach(p -> {
            p.setSysRoles(sysRoleService.listRolesByUserId(p.getId()));
        });
        return Results.succ(page);
    }
    @PreAuthorize("hasAuthority('sys:user:save')")
    @PostMapping("/save")
    public Results save(@Validated @RequestBody SysUser sysUser) {
        // Set the update time
        sysUser.setCreated(LocalDateTime.now());
        // The default state
        sysUser.setStatu(Const.STATUS_ON);
        // Set the default encrypted password
        String password = bCryptPasswordEncoder.encode(Const.PASS_WORD);
        sysUser.setPassword(password);
        // Set the default avatar
        sysUser.setAvatar(Const.Avatar);
        sysUserService.save(sysUser);
        return Results.succ(sysUser);
    }
    @PreAuthorize("hasAuthority('sys:user:update')")
    @PostMapping("/update")
    public Results update(@Validated @RequestBody SysUser sysUser) {
        // Set the update time
// sysUser.setCreated(LocalDateTime.now());
        sysUser.setUpdated(LocalDateTime.now());
        sysUserService.updateById(sysUser);
        return Results.succ(sysUser);
    }
    @PreAuthorize("hasAuthority('sys:user:delete')")
    @PostMapping("/delete")
    @Transactional
    public Results delete(@RequestBody Long [] ids) {
        sysUserService.removeByIds(Arrays.asList(ids));
        // Delete the middle relational table
        sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("user_id",ids));
        return Results.succ("");
    }
    @PreAuthorize("hasAuthority('sys:user:role')")
    @PostMapping("/role/{userId}")
    @Transactional
    public Results rolePerm(@PathVariable Long userId,@RequestBody Long [] roleIds) {
        ArrayList<SysUserRole> UserRoleList = new ArrayList<>();
        Arrays.stream(roleIds).forEach(r ->{
            SysUserRole userRole = new SysUserRole();
            userRole.setRoleId(r);
            userRole.setUserId(userId);
            UserRoleList.add(userRole);
        });
        // Delete the associated table data
        sysUserRoleService.remove(new QueryWrapper<SysUserRole>().eq("user_id",userId));
        sysUserRoleService.saveBatch(UserRoleList);
        // Delete the cache
        SysUser sysUser = sysUserService.getById(userId);
        sysUserService.clearUserAuthorityInfo(sysUser.getUsername());
        return Results.succ("");
    }

    @PostMapping("/repass")
    @PreAuthorize("hasAuthority('sys:user:repass')")
    public Results repass(@RequestBody Long id) {
        SysUser byId = sysUserService.getById(id);
        byId.setPassword(bCryptPasswordEncoder.encode(Const.PASS_WORD));
        byId.setCreated(LocalDateTime.now());
        sysUserService.save(byId);
        return Results.succ("");
    }
    // Personal center modification

    @PostMapping("/updatePass")
    public Results updatePass(@Validated @RequestBody PassDto passDto, Principal principal) {
        SysUser sysUser = sysUserService.getByUsername(principal.getName());
        boolean matches = bCryptPasswordEncoder.matches(passDto.getCurrentPass(), sysUser.getPassword());
        if(! matches) {return Results.fail("Incorrect password");
        }
        sysUser.setPassword(bCryptPasswordEncoder.encode(Const.PASS_WORD));
        sysUser.setUpdated(LocalDateTime.now());
        sysUserService.updateById(sysUser);
        return Results.succ(""); }}Copy the code

Top use a sysRoleService listRolesByUserId, through user id access to all of the associated role, with the middle table, can write SQL, so I write here

@Overridepublic List<SysRole> listRolesByUserId(Long userId) {   return this.list(         new QueryWrapper<SysRole>()               .inSql("id"."select role_id from sys_user_role where user_id = "+ userId)); }Copy the code

UserId must be found in their own database, do not let the front-end pass anything directly call this method, otherwise it may be attacked

The end of the

All the resources in the resources can be downloaded.

Refer to the up main add link description