赞
踩
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 (全称: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);
}
}
##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信息
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。