自学内容网 自学内容网

从零开始理解Spring Security的认证与授权

1、权限框架
1.1、概述

​ 权限管理是所有后台系统的都会涉及的一个重要组成部分,主要目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。那么如何实现权限呢?下面列举以下几个方案:

方案一:使用权限框架 Shiro

​ Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份 认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。Shiro最大的特点是 不跟任何的框架或者容器捆绑,可以独立运行。

​ 如果项目没有使用到Spring框架,可以考虑使用Shiro。Shiro在小项目使用比较常见。

​ Shiro 最大的问题在于和 Spring 家族的产品进行整合时较为不便。在Spring Boot 推出的很长一段时间里,Shiro 都没有提供相应的 starter,后来虽然有一个 shiro-spring-boot-web-starter 出来,但配置并没有简化多少。所以在 Spring Boot/Spring Cloud 技术栈的微服务项目中,Shiro 几乎不存在优势。

方案二:使用权限框架Spring Security

​ Spring Security是一个功能强大且高度可定制的,主要负责为Java程序提供声明式的身份验证和访问控制的安全框架。其前身是Acegi Security,后来被收纳为Spring的一个子项目,并更名为了Spring Security。

优点:

  • Spring Security基于Spring开发,所以Spring Security与Spring更契合;

  • Spring Security功能比Shiro更加强大,尤其是在安全防护方面;

  • Spring Security社区资源比Shiro更加丰富;

  • Spring Boot/Spring Cloud环境中,更容易集成Spring Security;

  • Spring Security 具备良好的扩展性,可以满足自定义的要求;

  • Spring Security对 OAuth2框架支持很好,而Shiro则对 OAuth2 支持不够。

方案三:使用拦截器(过滤器)+JWT 实现地址鉴权

方案四:使用AOP实现方法鉴权

2、核心概念

认证

​ 请求认证: 判断一个用户是否为合法用户的处理过程,最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确,如下图所示:

概念图解

授权

​ 授权就是用户登录后,控制用户是否有权限访问某些资源。如下图所示:

访问控制图解

3、SpringSecurity

​ Spring Security 是 Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

3.1、简单案例

创建一个空的Springboot项目

​ 这里只写了hello。跑起来,可以在浏览器输入http://localhost:8080/hello 会看到浏览器输出 hello ,目前这个工程,是没有权限控制的。

/**
 * @ClassName
 * @Description
 */
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }

}

运行图

pom.xml 引入依赖

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

重新启动项目

​ 浏览器输入 http://localhost:8080/hello 这时会自动跳转到一个默认登陆页面

​ 默认用户名是user,密码会输出在控制台。

在浏览器输入 http://localhost:8080/logout 退出登录。

​ demo就演示完成了,是不是很神奇呀!只是引入了一个依赖,什么代码都没写,就实现了一套简单的权限控制了,当然实际的项目,肯定不能这么做的

3.2、自定义登录页

编写自己的登录页面

创建配置类SecurityConfig,配置登录页

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.formLogin()             //自定义自己编写的登陆页面
                .loginPage("/login.html")    //登录页面设置
                .loginProcessingUrl("/login") //登录访问路径
                .permitAll()//登录页和登录访问路径无需登录也可以访问
                .and()
                .authorizeRequests()
                .antMatchers("/css/**","/images/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();    //关闭csrf防护
        return http.build();
    }

}

再次运行项目,会看到我们自己写的登录页面,变成下面这个样子啦

修改HelloController 的hello方法

@RequestMapping("/hello")
public String hello(){
    //认证成功,得到认证成功之后用户信息
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    return "hello  "+userName;
}

运行后效果如下:

3.3、SpringSecurity基本原理

Spring-Security其内部基础的处理方式就是通过过滤器来实现的,看下刚才的例子用到的一些过滤器,如图所示:

这几个过滤器都是干啥的呢?

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
  • FilterSecurityInterceptor:负责权限校验的过滤器。

当然,SpringSecurity过滤器不止这些,只需了解即可!

3.4、认证

​ 在简单案例中,其实已经是一个非常简单的认证,但是用户名是写死的,密码也需要从控制台查看,很显然实际中并不能这么做。下面,来实现基于内存模型的认证以及用户的自定义认证,密码加密等内容。

3.4.1、基于内存模型实现认证

修改配置类 SecurityConfig,添加两个bean的配置

@Bean
PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("123456")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("112233")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

Spring Security 提供了一个 UserDetails的实现类 User,用于用户信息的实例表示。另外,User 提供 Builder 模式的对象构建方式。

  • NoOpPasswordEncoder:这种编码器不进行任何编码,密码以明文形式存储。
  • InMemoryUserDetailsManager:管理存储在内存中的用户。

再次测试,输入用户名 user 密码123456

3.4.2、BCrypt密码加密

​ 明文密码肯定不安全,所以我们需要实现一个更安全的加密方式BCrypt。

​ BCrypt就是一款加密工具,可以比较方便地实现数据的加密工作。也可以简单理解为它内部自己实现了随机加盐处理。例如,使用MD5加密,每次加密后的密文其实都是一样的,这样就方便了MD5通过大数据的方式进行破解。
BCrypt生成的密文长度是60,而MD5的长度是32。

我们现在随便找个类,写个main方法测试一下

public static void main(String[] args) {
   String password = BCrypt.hashpw("000000", BCrypt.gensalt());
   System.out.println(password);
}

输出结果如下:

$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y

你运行的结果不是这样的,因为我们这里用了随机盐。

BCrypt提供了一个方法,用于验证密码是否正确。

boolean checkpw = BCrypt.checkpw("000000", "$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y");

接下来,我们看代码如何实现

(1)修改配置类SecurityConfig 的passwordEncoder实现类为BCryptPasswordEncoder

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

​ PasswordEncoder的实现类BCryptPasswordEncoder,用于BCrypt密码的解析。

(2)修改配置类SecurityConfig 的users方法中的密码,为加密后的密码

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
            .username("user")
            .password("$2a$10$2VCyByZ5oeiXCEN73wvBB.xpmJgPBbZVS/Aallmdyij2G7hmAKQTG")
            .roles("USER")
            .build();
    UserDetails admin = User.builder()
            .username("admin")
            .password("$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y")
            .roles("USER", "ADMIN")
            .build();
    return new InMemoryUserDetailsManager(user, admin);
}

(3)再次测试,输入用户名 user 密码123456

3.4.3、基于JDBC数据库实现认证

​ 在Spring Security框架中提供了一个UserDetailsService 接口,它的主要作用是提供用户详细信息。具体来说,当用户尝试进行身份验证时,UserDetailsService 会被调用,以获取与用户相关的详细信息。这些详细信息包括用户的用户名、密码、角色等

执行流程如下:

新创建一个UserDetailsServiceImpl,让它实现UserDetailsService ,代码如下

@Component
public class UserDetailsServiceImpl  implements UserDetailsService {


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if(username.equals("user")){
            UserDetails user= User.builder()
                    .username("user")                  .password("$2a$10$8V3UHgnnI/3RCKhg5aklz.sw448DP4.x9P2hFl/fnw99QU86POlgm")
                    .roles("USER")
                    .build();
            return user;
        }
        if(username.equals("admin")){
            UserDetails admin= User.builder()
                    .username("admin")                    .password("$2a$10$pCQMCKRUi7iUXGBd14a3oetcdgD1MwgKLenxXHidq1pcfmuva1QjW")
                    .roles("ADMIN","USER")
                    .build();
            return admin;
        }
        return null;
    }
}
  • 当前对象需要让spring容器管理,所以在类上添加注解@Component
  • 大家注意一下loadUserByUsername方法的返回值,叫做UserDetails,这也是框架给提供了保存用户的类,并且也是一个接口,如果我们有自定义的用户信息存储,可以实现这个接口,我们后边会详细讲解

既然以上能使用这个类来查询用户信息,那么我们之前在SecurityConfig中定义的用户信息,可以注释掉了,如下:

    /*
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.builder()
                .username("user")
                .password("$2a$10$2VCyByZ5oeiXCEN73wvBB.xpmJgPBbZVS/Aallmdyij2G7hmAKQTG")
                .roles("USER")
                .build();
        UserDetails admin = User.builder()
                .username("admin")
                .password("$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y")
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }*/

我们可以重启项目,然后进行测试,发现跟之前没什么区别,一样是实现了安全校验

当然我们最终不能把用户静态的定义在代码中的,我们需要到数据库去查询用户,我们可以直接使用我们项目中的用户表,实现的步骤如下:

  • 导入相关依赖(数据库、mybaits、lombok等)
  • 添加配置:连接数据库、mybatis配置等(application.yml)
  • 编写实体类和mapper
  • 改造UserDetailsServiceImpl(用户从数据库中获取)

pom 文件添加依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
<!--MySQL支持-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

application.yml添加数据库相关配置

#服务配置
server:
  #端口
  port: 8080
spring:
  application:
    name: springsecurity-demo
  #数据源配置
  datasource:
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://192.168.200.146:3306/security_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
# MyBatis配置
mybatis:
  #mapper配置文件
  mapper-locations: classpath*:mapper*/*Mapper.xml
  type-aliases-package: com.demo.project.entity
  configuration:
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 驼峰下划线转换
    map-underscore-to-camel-case: true
    use-generated-keys: true
    default-statement-timeout: 60
    default-fetch-size: 100

实体类和mapper

用户实体类

@Data
public class User {
    public Long id;

    /**
     * 用户账号
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 真实姓名
     */
    private String nickName;


}

用户mapper,我们只需要定义一个根据用户名查询的方法即可

@Mapper
public interface UserMapper {

    @Select("select * from sys_user where username = #{username}")
    public User findByUsername(String username);
}

改造UserDetailsServiceImpl

@Component
public class UserDetailsServiceImpl implements UserDetailsService {


    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //查询用户
        User user = userMapper.findByUsername(username);
        if(user == null){
            throw new RuntimeException("用户不存在或已被禁用");
        }
        SimpleGrantedAuthority user_role = new SimpleGrantedAuthority("user");
        SimpleGrantedAuthority admin_role = new SimpleGrantedAuthority("admin");
        List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();

        list.add(user_role);
        list.add(admin_role);

        return new org.springframework.security.core.userdetails.User(user.getUsername()
                ,user.getPassword()
                , list);
    }
}

上述代码中,返回的UserDetails或者是User都是框架提供的类,我们在项目开发的过程中,很多需求都是我们自定义的属性,我们需要扩展该怎么办?

其实,我们可以自定义一个类,来实现UserDetails,在自己定义的类中,就可以扩展自己想要的内容,如下代码:

@Data
public class UserAuth implements UserDetails {

    private String username; //固定不可更改
    private String password;//固定不可更改
    private String nickName;  //扩展属性  昵称
    private List<String> roles; //角色列表


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(roles==null) return null;
        //把角色类型转换并放入对应的集合
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_"+role)).collect(Collectors.toList());
    }

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

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

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

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

然后,我们可以继续改造UserDetailsServiceImpl中检验用户的逻辑,代码如下:

@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户
        User user = userMapper.findByUsername(username);
        if(user == null){
            throw new RuntimeException("用户不存在或已被禁用");
        }
        UserAuth userAuth = new UserAuth();
        userAuth.setUsername(user.getUsername());
        userAuth.setPassword(user.getPassword());
        userAuth.setNickName(user.getNickName());

        //添加角色
        List<String> roles=new ArrayList<>();
        if("user@qq.com".equals(username)){
            roles.add("USER");
            userAuth.setRoles(roles);
        }
        if("admin@qq.com".equals(username)){
            roles.add("USER");
            roles.add("ADMIN");
            userAuth.setRoles(roles);
        }
        return userAuth;
    }
}

修改HelloController,使用getPrincipal()方法读取认证主体对象。

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        //获取当前登录用户名称
        String name = SecurityContextHolder.getContext().getAuthentication().getName();
        UserAuth userAuth = (UserAuth)SecurityContextHolder.getContext().getAuthentication().getPrincipal();//取出认证主体对象

        return "hello :"+name+"  昵称:"+userAuth.getNickName();
    }

}
3.5、授权

​ 授权的方式包括 web授权和方法授权,web授权是通过 url 拦截进行授权,方法授权是通过方法拦截进行授权。如果同时使用 web 授权和方法授权,则先执行web授权,再执行方法授权,最后决策都通过,则允许访问资源,否则将禁止访问。方法授权是通过注解进行授权的,粒度较小,耦合度太高

简单的例子:

(1)修改HelloController,增加两个方法 (根据hello方法复制后修改即可),主要是为了方便后边进行测试

@RequestMapping("/hello/user")
public String helloUser(){
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String name = authentication.getName();
    return "hello-user  "+name;
}


@RequestMapping("/hello/admin")
public String helloAdmin(){
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String name = authentication.getName();
    return "hello-admin  "+name;
}

(2)修改 SecurityConfig 的securityFilterChain方法 ,添加对以上两个地址的角色控制

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    http.formLogin()             //自定义自己编写的登陆页面
        .loginPage("/login.html")    //登录页面设置
        .loginProcessingUrl("/login") //登录访问路径
        .permitAll()//登录页和登录访问路径无需登录也可以访问
        .and()
        .authorizeRequests()
        .antMatchers("/css/**","/images/**").permitAll()
        .antMatchers("/hello/user").hasRole("USER")
        .antMatchers("/hello/admin").hasAnyRole("ADMIN")
        .anyRequest().authenticated()
        .and()
        .csrf().disable();    //关闭csrf防护
    return http.build();
}

(3)分别以user 和admin用户登录,进行测试。

控制操作方法

上文只是将请求接口路径与配置的规则进行匹配,那匹配成功之后应该进行什么操作呢?Spring Security 内置了一些控制操作。

  • permitAll() 方法,所有用户可访问。
  • denyAll() 方法,所有用户不可访问。
  • authenticated() 方法,登录用户可访问。
  • anonymous() 方法,匿名用户可访问。
  • rememberMe() 方法,通过 remember me 登录的用户可访问。
  • fullyAuthenticated() 方法,非 remember me 登录的用户可访问。
  • hasIpAddress(String ipaddressExpression) 方法,来自指定 IP 表达式的用户可访问。
  • hasRole(String role) 方法, 拥有指定角色的用户可访问,传入的角色将被自动增加 “ROLE_” 前缀。
  • hasAnyRole(String… roles) 方法,拥有指定任意角色的用户可访问。传入的角色将被自动增加 “ROLE_” 前缀。
  • hasAuthority(String authority) 方法,拥有指定权限( authority )的用户可访问。
  • hasAnyAuthority(String… authorities) 方法,拥有指定任意权限( authority )的用户可访问。
// 如果用户具备 admin 权限,就允许访问。
.antMatchers("/hello/admin").hasAuthority("admin") 
// 如果用户具备给定权限中某一个,就允许访问。
.antMatchers("/admin/user").hasAnyAuthority("admin","user")
// 如果用户具备 user 权限,就允许访问。注意不需要手动写 ROLE_ 前缀,写了会报错
.antMatchers("/security/**").hasRole("user") 
//如果请求是指定的 IP 就允许访问。
.antMatchers("/admin/demo").hasIpAddress("192.168.200.129")
4、SpringSecurity整合JWT
4.1、前后端分离的权限方案

前面,实现的是非前后端分离情况下的认证与授权的处理,目前大部分项目,都是使用前后端分离的模式。那么前后端分离的情况下,我们如何使用SpringSecurity来解决权限问题呢?最常见的方案就是SpringSecurity+JWT 。

整体实现思路:

4.2、实现登录

1、新创建一个项目springsecurity-jwt,注意把上一个demo工程的代码和pom都拷贝过来

2、pom文件引入依赖

<!--JWT-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--工具包-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.0.M3</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

3、找一个生成jwt的工具类

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param dateOffset jwt过期时间(小时)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey , int dateOffset, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(DateUtil.offset(new Date(), DateField.HOUR_OF_DAY, dateOffset));

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        try {
            // 得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                    // 设置需要解析的jwt
                    .parseClaimsJws(token).getBody();
            return claims;
        } catch (Exception e) {
//            throw new AccessDeniedException("没有权限,请登录");
            throw new RuntimeException("没有权限,请登录");
        }
    }

}

4、创建LoginController

@RestController
@RequestMapping("security")
public class LoginController {

    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public String login(@RequestBody LoginDto loginDto){

        UsernamePasswordAuthenticationToken authentication
                =new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());

        Authentication authenticate = authenticationManager.authenticate(authentication);

        if( authenticate.isAuthenticated() ){ //认证通过
            Object principal = authenticate.getPrincipal();
            Map<String, Object> claims = new HashMap<>();
            claims.put("user",principal);
            String token = JwtUtil.createJWT("itcast",360000, claims);
            return token;
        }else{
            return "";
        }
    }
}

5、修改SecurityConfig

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests().antMatchers("/security/login").permitAll();
        http.csrf().disable();
        //返回
        return http.build();

    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

使用ApiFox测试

4.3、自定义授权管理器

执行流程

​ 当用户登录以后,携带了token访问后端,那么此时Spring Security框架就要对当前请求进行验证,验证包含了两部分,第一验证携带的token是否合法,第二验证当前用户是否拥有当前访问资源的权限。

自定义授权管理器TokenAuthorizationManager

@Component
public class TokenAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {

        //获取request
        HttpServletRequest request = requestAuthorizationContext.getRequest();
        //获取用户当前的请求地址
        String requestURI = request.getRequestURI();
        //获取token
        String token = request.getHeader("token");
        if(null == token || "".equals(token)){
            return new AuthorizationDecision(false);
        }
        //解析token
        Claims claims = JwtUtil.parseJWT("itcast", token);
        if (ObjectUtil.isEmpty(claims)) {
            //token失效
            return new AuthorizationDecision(false);
        }
        //获取userAuth
        UserAuth userAuth = JSONObject.parseObject(JSON.toJSONString(claims.get("user")),UserAuth.class);
        //存入上下文
        UsernamePasswordAuthenticationToken auth
                =new UsernamePasswordAuthenticationToken( userAuth, userAuth.getPassword(), userAuth.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(auth);

        //判断地址与对象中的角色是否匹配
        if(userAuth.getRoles().contains("ADMIN")){
            if("/hello/admin".equals(requestURI)){
                return new AuthorizationDecision(true);
            }
        }
        if(userAuth.getRoles().contains("USER")){
            if("/hello/user".equals(requestURI)){
                return new AuthorizationDecision(true);
            }
        }
        return new AuthorizationDecision(false);
    }
}

修改SecurityConfig,注册授权管理器

并同时关闭session和缓存,前后端分离项目不需要使用session和缓存

@Configuration
public class SecurityConfig {

    @Autowired
    private TokenAuthorizationManager tokenAuthorizationManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests().antMatchers("/security/login").permitAll()
                .anyRequest().access(tokenAuthorizationManager);
        
        //关闭session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //关闭缓存
        http.headers().cacheControl().disable();
        
        http.csrf().disable();
        //返回
        return http.build();

    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

测试一:

登录账号:user@qq.com 拥有角色:USER

可以访问:/hello/user

其他请求返回403

测试二:

登录账号:admin@qq.com 拥有角色:USER、ADMIN

可以访问:/hello/user、/hello/user

附单词表:

单词音标解释
Securitysəˈkjʊrəti安全
Authenticationɔːˌθentɪˈkeɪʃən身份认证,衍生词:Authenticated(被认证过的)
Authorizationˌɔːθəraɪˈzeɪʃən访问授权,衍生词:Authorize、Authority
Permitˈpɜːmɪt许可证
Matchersˈmætʃərz匹配器
Grantedɡræntɪd授予特定的权限
Principalˈprɪnsəpl被认证和授权访问资源或系统的实体或用户

原文地址:https://blog.csdn.net/weixin_53961667/article/details/143914773

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!