【SpringBoot+SpringSecurity+Jwt】前后分离认证授权完整教程

目录

  • 简介
  • 开工
    • 1.准备工作
      • 1.1 springboot工程(2.2.2.RELEASE)
      • 1.2 springsecurity
      • 1.3 缓存
      • 1.4 数据库mysql+连接池druid
      • 1.5 持久层 Mybatis-Plus
      • 1.6 常用工具
      • 1.7 完整pom
    • 2.认证
      • 2.1 用户信息
        • 2.1.1 用户实体类
        • 2.1.2 用户Mapper
        • 2.1.3 UserDetails自定义封装
        • 2.1.4 UserDetailsService的自定义实现
      • 2.2 密码解析器PasswordEncoder
        • 2.2.1 配置PasswordEncoder实例
        • 2.2.2 使用PasswordEncoder加密
      • 2.3 Jwt生成和解析token(hutool工具)
        • 2.3.1 jwt生成token
        • 2.3.2 jwt验证token
        • 2.3.3 jwt通过token获取载荷
        • 2.3.4 jwt续签token
      • 2.4 缓存(caffeine)
        • 2.4.1 配置
        • 2.4.2 简单使用
      • 2.5 登录
        • 2.5.1 登录接口
        • 2.5.2 AuthenticationManager 配置
        • 2.5.3 登录放行
        • 2.5.4 登录实现
        • 2.5.5 认证过滤器实现
        • 2.5.6 认证过滤器配置
        • 2.5.7 登出接口
        • 2.5.8 登出实现
    • 3.授权
      • 3.1 权限
        • 3.1.1 权限封装
        • 3.1.1 权限设计(基于BRAC权限模型)
        • 3.1.2 权限查询
      • 3.2 授权
        • 3.1.1 权限校验
        • 3.1.2 权限校验(用户自定义权限)
        • 3.1.3 权限校验(用户动态权限URI)
    • 4.异常处理
      • 4.1认证异常处理
      • 4.2 授权异常处理
      • 4.3 全局异常处理
      • 4.4 配置异常处理
      • 4.5 跨域处理
    • 5. 其他
      • 5.1 通用接口返回类封装
      • 5.2 XSS攻击防御

简介

SpringSecurity是Spring的安全框架,其核心功能包括认证和授权。

  • 认证:认证即系统判断用户的身份是否合法,合法可继续访问,不合法则拒绝访问。
  • 授权:授权即认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

开工

1.准备工作

1.1 springboot工程(2.2.2.RELEASE)

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

1.2 springsecurity

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

1.3 缓存

  • 主流方案为redis,本次案例以学习为目标,本着学习、简洁、快速、方便的原则,采用本地缓存方案caffeine,替代redis缓存实现。
 		<!--caffeine 暂时替代redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

1.4 数据库mysql+连接池druid

 		<!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.19</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

1.5 持久层 Mybatis-Plus

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

1.6 常用工具

  • lombok
 		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
  • hutool
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>
  • fastjson
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>

1.7 完整pom

  • 为便于使用,以下提供完整pom文件内容供参考,其中也整合了log4j2日志、swagger2等,需要自取,不需要可跳过。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>learning-project-02</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--caffeine 暂时替代redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>
        <!--webclient-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.19</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
        <!--commons-io-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.13.0</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <!--hu tool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>

        <dependency>
            <groupId>net.sourceforge.tess4j</groupId>
            <artifactId>tess4j</artifactId>
            <version>5.8.0</version>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

2.认证

2.1 用户信息

2.1.1 用户实体类
  • 代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
@ApiModel("用户实体类")
public class User implements Serializable {
    private static final long serialVersionUID = -1L;

    @ApiModelProperty("用户ID")
    @TableId(type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("用户名称")
    private String userName;
    @ApiModelProperty("用户密码")
    private String passWord;
    @ApiModelProperty("是否删除")
    @TableLogic
    @TableField(fill = FieldFill.INSERT)
    private Boolean deleted;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date createTime;
    @ApiModelProperty("更新时间")
    @TableField(fill = FieldFill.UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date updateTime;

}
2.1.2 用户Mapper
  • 代码
@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {

    @Select("select * from user where user_name = #{userName} ")
    User selectByUserName(@Param("userName") String userName);
}
2.1.3 UserDetails自定义封装
  • 创建LoginUser实现UserDetails
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
	//用户实体类
    private User user;
    //权限列表
    private List<String> permissions;
    //
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities == null) {
            authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        }
        return authorities;
    }
	//返回user用户密码	
    @Override
    public String getPassword() {
        return user.getPassWord();
    }
	//返回user用户名
    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

2.1.4 UserDetailsService的自定义实现
  • 创建UserDetailsServiceImpl实现UserDetailsService接口loadUserByUsername()方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	//根据用户名获取用户信息
        User user = userMapper.selectByUserName(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return new LoginUser(user, Arrays.asList());
    }
}

2.2 密码解析器PasswordEncoder

  • 生产环境中,用户密码一般以加密形式保存于数据库中,用户登录的明文密码一般使用密码解析器才能将加密密码与明文密码做比对
  • SpringSecurity使用的密文解析器PasswordEncoder
  • 官方推荐的密码解析器是 BCryptPasswordEncoder
2.2.1 配置PasswordEncoder实例
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
2.2.2 使用PasswordEncoder加密
    @Autowired
    private UserService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void addList() {
        User user = User.builder()
                .userName("张三").passWord(passwordEncoder.encode("123456"))
                .build();
        userService.save(user);
    }

2.3 Jwt生成和解析token(hutool工具)

  • JWT:轻量级、可扩展、可自包含的身份验证和授权机制

  • 三部分组成:头部(Header)、载荷(Payload)和签名(Signature)

  • Token:前后分离项目中,生成token令牌,验证token令牌

2.3.1 jwt生成token
@Slf4j
public class JwtUtil {

    //过期时间
    private static final int TTL = 2;
    //token秘钥(可自定义配置)
    private static final String KEY = "key";

    /**
     * 创建token
     * @param id 用户id
     * @return token
     */
    public static String createToken(String id) {
        DateTime issuedAt = DateTime.now();
        DateTime expiresAt = issuedAt.offsetNew(DateField.HOUR, TTL);

        HashMap<String, Object> payload = new HashMap<>(8);
        payload.put(JWTPayload.ISSUED_AT, issuedAt); //签发时间
        payload.put(JWTPayload.EXPIRES_AT, expiresAt); //过期时间
        payload.put(JWTPayload.NOT_BEFORE, issuedAt); //生效时间
        payload.put("id", id); //自定义载荷(本次使用用户id)

        return JWTUtil.createToken(payload, KEY.getBytes());
    }
}    
2.3.2 jwt验证token
    public static boolean verifyToken(String token) {
        JWT jwt = JWTUtil.parseToken(token).setKey(KEY.getBytes());
        return jwt.validate(0);
    }
2.3.3 jwt通过token获取载荷
    public static String getId(String token) {
        return JWTUtil.parseToken(token).setKey(KEY.getBytes()).getPayload().getClaim("id").toString();
    }
2.3.4 jwt续签token
    public static String reset(String token, String id) {
        JWT jwt = JWTUtil.parseToken(token).setKey(KEY.getBytes());
        long iat = Long.parseLong(jwt.getPayloads().get(JWTPayload.ISSUED_AT).toString());
        long exp = Long.parseLong(jwt.getPayloads().get(JWTPayload.EXPIRES_AT).toString());
        if (DateUtil.currentSeconds() > (iat + (exp - iat) / 2)) {
            return createToken(id);
        }
        return token;
    }

2.4 缓存(caffeine)

2.4.1 配置
@Configuration
@EnableCaching
public class CacheConfig implements WebMvcConfigurer {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(1000)
                .maximumSize(5000)
                .expireAfterWrite(24 * 60, TimeUnit.MINUTES)
                .build();
    }
}
2.4.2 简单使用
    @Test
    public void testCaffeineCache() {
        User user = User.builder()
                .userName("张三").passWord(passwordEncoder.encode("123456"))
                .build();
        //保存
        caffeineCache.put("10101",user);
        //获取
        User cacheUser = (User)caffeineCache.getIfPresent("1010");
    }

2.5 登录

2.5.1 登录接口
@Slf4j
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/system/login")
    public CommonResponse login(@RequestBody User user) {
        return loginService.login(user.getUserName(), user.getPassWord());
    }

}

2.5.2 AuthenticationManager 配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
}
2.5.3 登录放行
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //csrf关闭
        http.csrf().disable();
        //不使用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //允许登录接口匿名访问
        http.authorizeRequests().antMatchers("/system/login").anonymous();
        //除上述接口外所有请求需要认证授权
        http.authorizeRequests().anyRequest().authenticated();

    }
}
2.5.4 登录实现
@Slf4j
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    Cache<String, Object> caffeineCache;

    @Override
    public CommonResponse login(String userName, String passWord) {
        //获取Authentication对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, passWord);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //认证不通过
        if (authenticate == null) {
            throw new UsernameNotFoundException("登录失败");
        }
        //认证通过获用户信息
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
         //生成token
        String token = JwtUtil.createToken(userId);
        //缓存用户信息
        caffeineCache.put(userId, loginUser);
        //返回
        return CommonResponse.success(HttpStatus.OK.value(), "登录成功", token);
    }
    
}    
2.5.5 认证过滤器实现
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    Cache<String, Object> caffeineCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = request.getHeader("token");
        //token不存在,放行(其他过滤器来处理,不是Jwt过滤器的工作)
        if (StrUtil.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        //验证token
        try {
            if (!JwtUtil.verifyToken(token)) {
                WebUtil.renderString(response, JSON.toJSONString(CommonResponse.error("用户凭证已过期")));
                return;
            }
        } catch (Exception e) {
            WebUtil.renderString(response, JSON.toJSONString(CommonResponse.error("非法认证")));
            return;
        }
        //获取用户id
        String id;
        try {
            id = JwtUtil.getId(token);
            if (StrUtil.isBlank(id)) {
                WebUtil.renderString(response, JSON.toJSONString(CommonResponse.error("用户信息不存在")));
                return;
            }
        } catch (Exception e) {
            WebUtil.renderString(response, JSON.toJSONString(CommonResponse.error("用户信息异常")));
            return;
        }
        //续写token
        String newToken = JwtUtil.reset(token, id);
        response.addHeader("token", newToken);
        //缓存获取用户信息
        LoginUser loginUser = (LoginUser) caffeineCache.getIfPresent(id);
        if (Objects.isNull(loginUser)) {
            WebUtil.renderString(response, JSON.toJSONString(CommonResponse.error("用户登录已过期")));
            return;
        }
        //设置用户信息
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,
                null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);

    }
}
  • **响应工具类WebUtil **
public class WebUtil {
    public static String renderString(HttpServletResponse response, String msg) {
        try {
            response.setStatus(HttpStatus.OK.value());
            response.setContentType(ContentType.JSON.getValue());
            response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
            response.getWriter().print(msg);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }
}
2.5.6 认证过滤器配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //csrf关闭
        http.csrf().disable();
        //不使用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //请求
        http.authorizeRequests()
                .antMatchers("/system/login").anonymous();
        http.authorizeRequests().anyRequest().authenticated();
        //token校验
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
       
    }
2.5.7 登出接口
@Slf4j
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/system/logout")
    public CommonResponse logout() {
        return loginService.logout();
    }

}

2.5.8 登出实现
@Slf4j
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    Cache<String, Object> caffeineCache;

    @Override
    public CommonResponse logout() {
        //获取Authentication对象
        UsernamePasswordAuthenticationToken authenticate = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        //获用户信息
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        //清除缓存用户信息
        caffeineCache.invalidate(userId);
        //返回
        return CommonResponse.success(HttpStatus.OK.value(), "注销成功");
    }
}

3.授权

3.1 权限

3.1.1 权限封装
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
    private List<String> permissions;
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities == null) {
            authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        }
        return authorities;
    }
}
3.1.1 权限设计(基于BRAC权限模型)
  • 用户表
CREATE TABLE `user` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(128) NOT NULL,
  `pass_word` varchar(128) DEFAULT NULL,
  `deleted` tinyint(1) unsigned DEFAULT '0',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
  • 角色表
CREATE TABLE `role` (
  `id` bigint(16) NOT NULL,
  `name` varchar(255) NOT NULL,
  `deleted` tinyint(1) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 菜单表
CREATE TABLE `menu` (
  `id` bigint(16) NOT NULL AUTO_INCREMENT,
  `menu_name` varchar(128) NOT NULL,
  `perms` varchar(255) DEFAULT NULL,
  `deleted` tinyint(1) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 用户-角色表
CREATE TABLE `user_role` (
  `user_id` bigint(16) NOT NULL,
  `role_id` bigint(16) NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 菜单-角色表
CREATE TABLE `role_menu` (
  `role_id` bigint(16) NOT NULL,
  `menu_id` bigint(16) NOT NULL,
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3.1.2 权限查询
  • 菜单实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName("menu")
@ApiModel("菜单实体类")
public class Menu implements Serializable {
    private static final long serialVersionUID = -1;

    @ApiModelProperty("菜单ID")
    @TableId(type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("菜单名称")
    private String menuName;
    @ApiModelProperty("权限标识")
    private String perms;
    @ApiModelProperty("是否删除")
    @TableLogic
    private Boolean deleted;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date createTime;
    @ApiModelProperty("更新时间")
    @TableField(fill = FieldFill.UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date updateTime;
}
  • 菜单Mapper
@Mapper
@Repository
public interface MenuMapper extends BaseMapper<Menu> {

    @Select("select " +
            "   distinct m.perms " +
            "from " +
            "   user_role ur " +
            "   left join role r on ur.role_id = r.id " +
            "   left join role_menu rm on ur.role_id = rm.role_id " +
            "   left join menu m on rm.menu_id = m.id " +
            "where " +
            "   ur.user_id = #{userId} ")
    List<String> selectPermsByUserId(@Param("userId") Long userId);
}
  • 权限集合查询
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectByUserName(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<String> perms = menuMapper.selectPermsByUserId(user.getId());

        return new LoginUser(user, perms);
    }
}

3.2 授权

3.1.1 权限校验
    @GetMapping("/hello1")
    @PreAuthorize("hasAnyAuthority('admin','test:list')")
    public String hello1() {
        return "hello1";
    }
3.1.2 权限校验(用户自定义权限)
    @GetMapping("/hello2")
    @PreAuthorize("@root.hasAuthority('test:list')")
    public String hello2() {
        return "hello2";
    }
@Component("root")
public class RootAuthorize {

    public boolean hasAuthority(String auth) {
        //获取当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        return permissions.contains(auth);
    }
}
3.1.3 权限校验(用户动态权限URI)
    @GetMapping("/hello3")
    @PreAuthorize("@root.hasAuthority(#request.getRequestURI())")
    public String hello3(HttpServletRequest request) {
        return "hello3";
    }

4.异常处理

4.1认证异常处理

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException e) throws IOException, ServletException {
        // 返回401 Unauthorized错误码
        WebUtil.renderString(response,
                JSON.toJSONString(CommonResponse.success(HttpStatus.UNAUTHORIZED.value(), e.getMessage())));
    }
}

4.2 授权异常处理

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        //返回403 Forbidden错误
        WebUtil.renderString(response,
                JSON.toJSONString(CommonResponse.success(HttpStatus.FORBIDDEN.value(), e.getMessage())));
    }
}

4.3 全局异常处理

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(AuthenticationException.class)
    public void authenticationException(AuthenticationException e) throws AuthenticationException {
        throw e;
    }

    @ExceptionHandler(AccessDeniedException.class)
    public void authenticationException(AccessDeniedException e) throws AccessDeniedException {
        throw e;
    }

    @ExceptionHandler(Exception.class)
    public CommonResponse exception(Exception e) {
        log.error("[Exception] message: {}", e.getMessage());
        return CommonResponse.error("系统异常");
    }

    @ExceptionHandler(RuntimeException.class)
    public CommonResponse runtimeException(RuntimeException e) {
        log.error("[Exception] message: {}", e.getMessage());
        return CommonResponse.error(e.getMessage());
    }

    @ExceptionHandler(NullPointerException.class)
    public CommonResponse nullPointerException(NullPointerException e) {
        log.error("[NullPointerException] message: {}", e.getMessage());
        return CommonResponse.error("空指针异常");
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    public CommonResponse indexOutOfBoundsException(IndexOutOfBoundsException e) {
        log.error("[IndexOutOfBoundsException] message: {}", e.getMessage());
        return CommonResponse.error("下标越界异常");
    }

    @ExceptionHandler(NoSuchMethodException.class)
    public CommonResponse noSuchMethodException(NoSuchMethodException e) {
        log.error("[NoSuchMethodException] message: {}", e.getMessage());
        return CommonResponse.error("未找到对应方法异常");
    }

    @ExceptionHandler(IOException.class)
    public CommonResponse ioException(IOException e) {
        log.error("[IOEXCEPTION] message: {}", e.getMessage());
        return CommonResponse.error("IO异常");
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CommonResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("[MethodArgumentNotValidException] message: {}", e.getMessage());
        return CommonResponse.error("参数校验异常");
    }

}

4.4 配置异常处理

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //csrf关闭
        http.csrf().disable();
        //不使用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //请求
        http.authorizeRequests()
                .antMatchers("/system/login").anonymous();
        http.authorizeRequests().anyRequest().authenticated();

        http.formLogin().loginPage("/").permitAll();
        //token校验
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }

4.5 跨域处理

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //csrf关闭
        http.csrf().disable();
        //不使用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //请求
        http.authorizeRequests()
                .antMatchers("/system/login").anonymous();
        http.authorizeRequests().anyRequest().authenticated();

        http.formLogin().loginPage("/").permitAll();
        //token校验
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
       //跨域
        http.cors().disable();
    }

5. 其他

5.1 通用接口返回类封装

@Data
@Builder
@ApiModel("API通用返回数据实体类")
public class CommonResponse implements Serializable {
    private static final long serialVersionUID = -1;

    @ApiModelProperty("代码")
    private int code;

    @ApiModelProperty("提示信息")
    private String msg;

    @ApiModelProperty("数据")
    private Object data;

    public CommonResponse(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static CommonResponse success() {
        return new CommonResponse(HttpStatus.OK.value(), HttpStatus.OK.name(), null);
    }

    public static CommonResponse success(String msg) {
        return new CommonResponse(HttpStatus.OK.value(), msg, null);
    }

    public static CommonResponse success(int code, String msg) {
        return new CommonResponse(code, msg, null);
    }

    public static CommonResponse success(Object data) {
        return new CommonResponse(HttpStatus.OK.value(), HttpStatus.OK.name(), data);
    }

    public static CommonResponse success(int code, String msg, Object data) {
        return new CommonResponse(code, msg, data);
    }

    public static CommonResponse error() {
        return new CommonResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.name(), null);
    }

    public static CommonResponse error(String msg) {
        return new CommonResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, null);
    }

    public static CommonResponse error(int code, String msg) {
        return new CommonResponse(code, msg, null);
    }
}

5.2 XSS攻击防御

  • Xss过滤器
@Component
@WebFilter(urlPatterns = "/*")
public class XssFilter implements Filter {

    public static final List<String> EXCLUDE_URL_PATTERNS = Arrays.asList("/error", "/actuator",
            "/swagger-ui.html", "/swagger-resources", "/webjars", "/v2/api-docs");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        if (StrUtil.isNotBlank(request.getRequestURI()) && EXCLUDE_URL_PATTERNS.contains(
                StrUtil.sub(
                        request.getRequestURI().replace("//", "/"), 0,
                        StrUtil.indexOf(request.getRequestURI().replace("//", "/"), '/', 1) > 0 ?
                                StrUtil.indexOf(request.getRequestURI().replace("//", "/"), '/', 1) :
                                request.getRequestURI().replace("//", "/").length()))
        ) {
            filterChain.doFilter(request, response);
        } else {
            XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(request);
            filterChain.doFilter(wrapper, response);
        }

    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}
  • Xss防御
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private byte[] requestBody;

    public XssHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        try {
            requestBody = StreamUtils.copyToByteArray(request.getInputStream());
        } catch (Exception e) {
            throw new IOException("Xss拦截异常");
        }
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        if (StrUtil.isNotBlank(value)) {
            value = HtmlUtil.cleanHtmlTag(value).trim();
        }
        return value;
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (StrUtil.isNotBlank(value)) {
            value = HtmlUtil.cleanHtmlTag(value).trim();
        }
        return value;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null || values.length <= 0) {
            return super.getParameterValues(name);
        }
        for (int i = 0; i < values.length; i++) {
            if (StrUtil.isNotBlank(values[i])) {
                values[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
            }
        }
        return values;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        LinkedHashMap<String, String[]> map = new LinkedHashMap<>();
        Map<String, String[]> parameterMap = super.getParameterMap();
        if (parameterMap == null) {
            return super.getParameterMap();
        }
        for (String key : parameterMap.keySet()) {
            String[] values = parameterMap.get(key);
            for (int i = 0; i < values.length; i++) {
                if (StrUtil.isNotBlank(values[i])) {
                    values[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
                }
            }
            map.put(key, values);
        }
        return map;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        //空直接返回
        if (requestBody == null) {
            requestBody = new byte[0];
        }
        //非json直接返回
        if (!StrUtil.startWithIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JSON_VALUE)) {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
            return getServletInputStream(byteArrayInputStream);
        }

        StringBuilder body = new StringBuilder();
        ByteArrayInputStream in = new ByteArrayInputStream(requestBody);
        InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
        BufferedReader buffer = new BufferedReader(reader);
        String line = buffer.readLine();
        while (line != null) {
            body.append(line);
            line = buffer.readLine();
        }
        buffer.close();
        reader.close();
        in.close();

        Map<String, Object> result = new LinkedHashMap<>();

        Map<String, Object> jsonMap = JSONUtil.parseObj(body.toString());
        for (String key : jsonMap.keySet()) {
            Object value = jsonMap.get(key);
            if (value instanceof String) {
                if (StrUtil.isNotBlank(value.toString())) {
                    result.put(key, HtmlUtil.cleanHtmlTag(value.toString()).trim());
                } else {
                    result.put(key, value);
                }
            } else {
                result.put(key, value);
            }
        }

        String json = JSONUtil.toJsonStr(result);
        ByteArrayInputStream bain = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
        return getServletInputStream(bain);
    }

    private ServletInputStream getServletInputStream(ByteArrayInputStream bain) {
        return new ServletInputStream() {
            @Override
            public int read() {
                return bain.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}

你可能感兴趣的:(spring,boot,后端,java)