spring security6+springboot3+jwt实现权限控制

maven配置

myabits-plus,redis,lombok,hutool

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.5</version>
        <relativePath/> <!-- lookup parent from repository -->
</parent>
 
 
<properties>
        <java.version>17</java.version>
    </properties>
 
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
 
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</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-web</artifactId>
        </dependency>
        <!--        redis依赖start-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!-- Mysql驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
    </dependencies>

授权核心类配置

spring security的核心配置,包含了自定义的鉴权

@Configuration
@RequiredArgsConstructor
public class AuthConfig {
 

    /**
     * 自定义的权限读取类
     */
    private final MyUserDetailService myUserDetailService;
 

    /**
     * springsecurity权限校验提供类
     
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        //使用自定义的用户校验
        authProvider.setUserDetailsService(myUserDetailService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
 
    
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
 
    /**
     * 密码加密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

MyUserDetailService 自定义的权限读取类
AuthenticationProvider 提供权限校验方法类,把MyUserDetailService类和PasswordEncoder 类作为入参,实现自定义权限校验

AuthenticationManager 鉴权工具的管理bean,用默认提供的就行

PasswordEncoder 使用BCryptPasswordEncoder作为我们密码加密的方法

用户详细权限获取类

MyUserDetailService是我们实现自定义权限认证的核心方法之一,通过实现UserDetailsService的loadUserByUsername方法实现获取用户拥有的详细权限,然后springsecurity框架会帮助我们对需要拦截的请求进行权限校验

创建我们的一个用户类,然后实现UserDetails接口,UserDetails是springsecurity框架的授权用户实体,我们需要继承这个接口才能让框架去为我们实现权限校验,这两个类都需要去继承框架的接口,这样才能让框架帮我们管理

 
##class MyUserDetailService 
 
 
@RequiredArgsConstructor
@Service
@Slf4j
public class MyUserDetailService implements UserDetailsService, UserDetailsPasswordService {
 
 
    //获取当前用户的方法,使用框架的上下文获取当前请求的用户
    public static Authentication getCurrentUser() {
        return SecurityContextHolder.getContext().getAuthentication();
    }
 
    //刷新密码,如果用户更改密码了会重新刷新token或者退出登录,我也没用过,大家可以试一下
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("进入查找用户的方法");
        UserDO userDO = new UserDO().selectOne(new LambdaQueryWrapper<UserDO>().eq(UserDO::getUsername, username));
        if (userDO != null) {
            //从数据库中获取用户,真实环境下需要获取用户拥有的权限,现在我们就先写死
            Set<SimpleGrantedAuthority> permissions = Set.of("admin:read", "admin:create", "admin:update", "admin:delete").stream().map(permission -> new SimpleGrantedAuthority(permission)).collect(Collectors.toSet());
            //返回生成的用户
            var sysUser = new SysUser().setId(1).setPassword(userDO.getPassword()).setUsername(username).setAuthorities(permissions);
            return sysUser;
        }
        return null;
 
    }
}
 
 
##class SysUser 
 
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class SysUser implements UserDetails, Serializable {
 
    private Integer id;
 
    private String username;
 
    private String password;
 
 
    //该用户所拥有的权限,如果细分为角色和权限,可以把两个都放到这个集合里面,比如ROLE_ADMIN,admin:user:create可以同时存入
    private Collection<? extends GrantedAuthority> authorities;
 
 
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }
 
    @Override
    public String getPassword() {
        return this.password;
    }
 
    @Override
    public String getUsername() {
        return this.username;
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
}

JWT加解密方法

JWT (全称:Json Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。开发者可以将在每个服务间传递的信息(比如用户的id,name,权限集合等)通过加密生成一个token,然后在服务端解析出这个token就可以获取到这个请求携带的用户的信息。

可以通过JwtService类来对token进行加解密


##application.yml
 
application:
  security:
    jwt:
      secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
      expiration: 86400000 # a day
      refresh-token:
        expiration: 604800000 # 7 days
 
 
 
 
 
 
 
 
## class JwtService
 
 
@Service
public class JwtService {
 
  @Value("${application.security.jwt.secret-key}")
  private String secretKey;
  @Value("${application.security.jwt.expiration}")
  private long jwtExpiration;
  @Value("${application.security.jwt.refresh-token.expiration}")
  private long refreshExpiration;
 
  public String extractUsername(String token) {
    return extractClaim(token, Claims::getSubject);
  }
 
  public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = extractAllClaims(token);
    return claimsResolver.apply(claims);
  }
 
  public String generateToken(UserDetails userDetails) {
    return generateToken(new HashMap<>(), userDetails);
  }
 
  public String generateToken(
      Map<String, Object> extraClaims,
      UserDetails userDetails
  ) {
    return buildToken(extraClaims, userDetails, jwtExpiration);
  }
 
  public String generateRefreshToken(
      UserDetails userDetails
  ) {
    return buildToken(new HashMap<>(), userDetails, refreshExpiration);
  }
 
  private String buildToken(
          Map<String, Object> extraClaims,
          UserDetails userDetails,
          long expiration
  ) {
    return Jwts
            .builder()
            .setClaims(extraClaims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSignInKey(), SignatureAlgorithm.HS256)
            .compact();
  }
 
  public boolean isTokenValid(String token, SysUser sysUser) {
    final String username = extractUsername(token);
    return (username.equals(sysUser.getUsername())) && !isTokenExpired(token);
  }
 
  private boolean isTokenExpired(String token) {
    return extractExpiration(token).before(new Date());
  }
 
  private Date extractExpiration(String token) {
    return extractClaim(token, Claims::getExpiration);
  }
 
  private Claims extractAllClaims(String token) {
    return Jwts
        .parserBuilder()
        .setSigningKey(getSignInKey())
        .build()
        .parseClaimsJws(token)
        .getBody();
  }
 
  private Key getSignInKey() {
    byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    return Keys.hmacShaKeyFor(keyBytes);
  }
}

SecurityConfig

 
##class SecurityConfig 
 
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {
 
 
    /**
     * AuthConfig中自定义的bean
     */
    private final AuthenticationProvider authenticationProvider;
 
    private final JwtFilter jwtFilter;
 
    private final LogoutService logoutService;
 
    private final MyAccessDeniedHandler myAccessDeniedHandler;
 
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf()//关闭session
                .disable()
                .authorizeHttpRequests()
                .requestMatchers("/api/v1/auth/**")//  放行/api/v1/auth/下的所有请求,主要是登录登出
                .permitAll()
 
                .anyRequest()
                .authenticated()//  剩下的请求都需要拦截
                .and()
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler) //  配置无权访问的处理
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authenticationProvider(authenticationProvider)//  使用我们自定义的权限查询校验方法
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) //  配置请求拦截
                .logout()
                .logoutUrl("/api/v1/auth/logout") //   配置登出路径
                .addLogoutHandler(logoutService)
                .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()) // 登出后清楚请求的上下文,也就是用户信息
        ;
 
        return http.build();
    }
}
 
 
##class LogoutService
 
/**
 * 登出具体实现类
 */
@Component
public class LogoutService implements LogoutHandler {
 
 
    /**
     * 登出实现
     * @param request
     * @param response
     * @param authentication
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        //先获取token
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
            return;
        }
        jwt = authHeader.substring(7);
        //从db或者redis中获取token并且对比
        TokenDO tokenDO = new TokenDO().selectOne(new LambdaQueryWrapper<TokenDO>().eq(TokenDO::getToken, jwt));
        if (tokenDO != null) {
            tokenDO.setExpired(true);
            tokenDO.setRevoked(true);
            tokenDO.insertOrUpdate();
            SecurityContextHolder.clearContext();
        }
 
    }
}
 
 
##class MyAccessDeniedHandler
 
 
/**
 * 自定义无权访问处理类
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
 
 
    @Override
    @SneakyThrows(UnAuthorizationException.class)
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)  {
        //这里是想直接抛出一个无权访问的异常然后全局捕捉,但是不知道为什么没有捕捉到
        throw new UnAuthorizationException();
    }
}

springsecurity相关的一些配置都已经完成

用户相关类

用户实体类,同时需要写一个mapper来对应实体类 

##  class   UserDO
@Data
@TableName("sys_user")
@EqualsAndHashCode(callSuper = true)
public class UserDO extends Model<UserDO> {
 
 
    @TableId(type = IdType.AUTO)
    private Integer id;
 
    private String username;
 
    private String password;
 
    private String name;
}
##  class UserMapper
@Repository
public interface UserMapper extends BaseMapper<UserDO> {
}

用户业务处理 

##  class UserService
 
public interface UserService {
 
    AjaxResult login(String username,String password);
}
 
 
 
 
##  class UserServiceImpl
 
 
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
 
 
    private final PasswordEncoder passwordEncoder;
 
    private final JwtService jwtService;
 
    private final StringRedisTemplate stringRedisTemplate;
 
    private final int tokenTimeOut=60*1000;
 
 
    /**
     * 用户登录
     *
     * @param username
     * @param password
     * @return
     */
    @Override
    public AjaxResult login(String username, String password) {
        //简单测试security内容暂时不考虑校验用户名密码复杂度等
        UserDO userDO = new UserDO().selectOne(new LambdaQueryWrapper<UserDO>().eq(UserDO::getUsername, username));
        //校验密码是否正确
        if (passwordEncoder.matches(password, userDO.getPassword())) {
            //用户校验成功,从数据库中查询出用户的权限,现在为了方便测试直接写死
            Set<SimpleGrantedAuthority> permissions = Set.of("admin:read", "admin:create", "admin:update", "admin:delete").stream().map(permission -> new SimpleGrantedAuthority(permission)).collect(Collectors.toSet());
            //生成用户的token
            var sysUser = new SysUser().setId(1).setPassword(userDO.getPassword()).setUsername(username).setAuthorities(permissions);
            var accssToken = jwtService.generateToken(sysUser);
            var refreshToken = jwtService.generateRefreshToken(sysUser);
            //此处有两个选择,可以把token存到db或者redis中,这里方便演示两个都保存
            new TokenDO().setToken(accssToken).setUserId(userDO.getId()).setRevoked(false).setExpired(false).insert();
            //可以配合jwt设置一个超时时间双重判断是否过期,也可以作为手动使token失效的功能
            stringRedisTemplate.opsForValue().set(StrUtil.format(OauthTestApplication.REDIS_TOKEN_KEY,accssToken), JSONUtil.toJsonStr(sysUser));
 
            //返回生成的两个token
            return AjaxResult.success(TokenResponse.builder().accessToken(accssToken).refreshToken(refreshToken).build());
        }
        return AjaxResult.error();
    }
}

最后是我们的接口控制层

在springsecurity中我们是用@PreAuthorize注解来控制该接口的权限@PreAuthorize("hasAuthority('admin:create')")标识拥有admin:create权限的用户才可以访问

 @RestController
@RequiredArgsConstructor
public class TestController {
 
 
    private final UserService userService;
 
    private final PasswordEncoder passwordEncoder;
 
 
    @PostMapping("/api/v1/auth/login")
    public AjaxResult login(String username, String password) {
        String encode = passwordEncoder.encode("123456");
        return userService.login(username, password);
    }
 
 
    @GetMapping("/api/v1/testAuth")
    @PreAuthorize("hasAuthority('admin:create')")
    public AjaxResult testAuth() {
        System.out.println("当前用户为:" + MyUserDetailService.getCurrentUser().getPrincipal());
        return AjaxResult.success("本接口你有权限");
    }
 
 
    @GetMapping("/api/v1/testUnAuth")
    @PreAuthorize("hasAuthority('admin:noauth')")
    public AjaxResult testUnAuth() {
        System.out.println("当前用户为:" + MyUserDetailService.getCurrentUser().getPrincipal());
        return AjaxResult.success("本接口你没有权限");
    }
 
    @GetMapping("/api/v1/testNoAuth")
    public AjaxResult testNoAuth() {
        System.out.println("当前用户为:" + MyUserDetailService.getCurrentUser().getPrincipal());
        return AjaxResult.success("本接口不需要权限");
    }
 
 
    @RequestMapping("/pass")
    public void pass(String password) {
        System.out.println("生成密码:"+passwordEncoder.encode(password));
    }
}

首先不登录测试我们的三个接口

localhost:8080/api/v1/testAuth

localhost:8080/api/v1/testUnAuth

localhost:8080/api/v1/testNoAuth

可以看到3个接口都没有返回,状态码都是403。403 Forbidden是HTTP协议中的一个状态码(Status Code)。可以理解为没有权限访问此站。该状态表示服务器理解了本次请求但是拒绝执行该任务,该请求不该重发给服务器。

然后我们进行登录操作

localhost:8080/api/v1/auth/login?username=admin&password=123456

 成功获取到token了,在redis中也能看到我们的token信息

 

 

 

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值