先导入redis、JWT、SpringSecurity的相关依赖。
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--JWT令牌-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
RBAC 是基于角色的访问控制在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,
而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
/**
*
* @author 淡薄愁雾
* @date 2024-02-04
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable, UserDetails {
private int id;
private String account;
@JsonIgnore //由于访问时会进行Json互转会报错,密码加密所以需要忽略
private String password;
private Role role;
@JsonIgnore
@Override
public Collection extends GrantedAuthority> getAuthorities() {
//将用户的角色权限信息封装成List
List authorities = new ArrayList<>();
//封装角色
SimpleGrantedAuthority role = new SimpleGrantedAuthority(this.role.getName());
authorities.add(role);
//封装权限
this.role.getPermsList().forEach(perms -> authorities.add(new SimpleGrantedAuthority(perms.getName())));
return authorities;
}
@Override
public String getUsername() {
return account;
}
//账号是否没有过期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号是否没有被锁定
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
//凭证是否没有过期(密码)
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
/**
*
* @author 淡薄愁雾
* @date 2024-02-04
*/
@Data
public class Role {
private Integer id;
private String name;
@JsonIgnore
private String description;
private List permsList;
}
/**
*
* @author 淡薄愁雾
* @date 2024-02-04
*/
@Data
public class Perms {
private Integer id;
private String name;
private String description;
}
用于生成JWT令牌及常用Token常量
/**
* @author 淡薄愁雾
* @date 2024-02-04 21:50
*/
public class JWTUtil {
public static final String SECRET_KEY = "123456"; //秘钥
public static final long TOKEN_EXPIRE_TIME = 24 * 3600 * 1000L; //token过期时间
public static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 3600 * 1000L; //refreshToken过期时间
private static final String ISSUER = "issuer"; //签发人
/**
* 生成签名
*/
public static String generateToken(int uid, String account){
Date now = new Date();
//创建签名算法对象
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法
String token = JWT.create()
.withIssuer(ISSUER) //签发人
.withIssuedAt(now) //签发时间
.withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME)) //过期时间
.withClaim("uid", uid) //保存身份标识
.withClaim("account", account) //保存身份标识
.sign(algorithm);
return token;
}
/**
* 验证token
*/
public static TokenEnum verify(String token){
try {
//签名算法
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build();
verifier.verify(token);
return TokenEnum.TOKEN_SUCCESS;
} catch (TokenExpiredException ex){
return TokenEnum.TOKEN_EXPIRE;
//ex.printStackTrace();
} catch (Exception e) {
return TokenEnum.TOKEN_BAD;
}
}
/**
* 从token获取uid
*/
public static int getuid(String token){
try{
return JWT.decode(token).getClaim("uid").asInt();
}catch(Exception ex){
ex.printStackTrace();
}
return 0;
}
public static String getAccount(String token){
try{
return JWT.decode(token).getClaim("account").asString();
}catch(Exception ex){
ex.printStackTrace();
}
return null;
}
}
编写Token常量(可以直接写)
public enum TokenEnum {
TOKEN_EXPIRE,TOKEN_BAD,TOKEN_SUCCESS
}
package com.smiao.vegetable.config;
import com.smiao.vegetable.exception.CustomAccessDeniedExceptionHandle;
import com.smiao.vegetable.filter.AuthFilter;
import com.smiao.vegetable.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.web.filter.CharacterEncodingFilter;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启security的注解权限管理
public class CustomSpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Resource
private AuthFilter authFilter;
/**
* 配置密码编码器
* NoOpPasswordEncoder: 不加密看需求
*/
@Bean
public PasswordEncoder passwordEncoder(){
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
/**
* 重写方法:指定当前系统的用户信息来自哪里
* 数据库,开发
*/
@Resource
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定用户信息来自数据库
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
/**
* 配置那个uri能被那些身份访问
*
*/
@Resource
private CustomAccessDeniedExceptionHandle customAccessDeniedExceptionHandle;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.antMatchers("/user/login","/images/**")
.permitAll() // permitAll()不需要登录就能访问
.anyRequest().authenticated()//其他的uri。必须要登录才能访问
.and()
.formLogin() // 开启表单登录
.and()
.exceptionHandling() //开启异常处理
.accessDeniedHandler(customAccessDeniedExceptionHandle)
.and()
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.csrf().disable(); //关闭csrf防御
}
// 创建认证管理器对象
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// 配置编码过滤器,用户数据中如果有中文会报错
String allowedPattern = ".*";
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowedHeaderValues((header) -> {
String parsed = new String(header.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
return parsed.matches(allowedPattern);
});
return firewall;
}
}
package com.smiao.vegetable.controller;
import com.smiao.vegetable.entity.User;
import com.smiao.vegetable.response.ResponseResult;
import com.smiao.vegetable.service.UserService;
import com.smiao.vegetable.util.JWTUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
*
* @author one piece
* @date 2024-02-04 22:12
*/
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@Resource
private RedisTemplate redisTemplate;
@PostMapping("/login")
public ResponseResult login(@RequestBody User user1, HttpServletResponse response){
// 登录
User result = userService.login(user1);
//进行密码比较
boolean matches = new BCryptPasswordEncoder().matches(user1.getPassword(), result.getPassword());
//密码相同,登录成功
if(matches){
// 生成token
String token = JWTUtil.generateToken(result.getId(), result.getAccount());
String refreshtoken = UUID.randomUUID().toString();
// 放入redis并设置过期时间
redisTemplate.opsForValue().set(refreshtoken, token, JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
// 返回给前端:通过自定义响应头的方式返回
response.setHeader("Authorization", token);
response.setHeader("RefreshToken", refreshtoken);
// 暴露头:为了不让浏览器忽略这个头信息
response.setHeader("Access-Control-Expose-Headers","Authorization,RefreshToken");
return new ResponseResult<>(ResponseResult.SUCCESS,"success", result);
}
return new ResponseResult<>(ResponseResult.LOGIN_FAIL,"用户名或密码有误", null);
}
}
Mapper
package com.smiao.vegetable.mapper;
import com.smiao.vegetable.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @Author: one piece
* @Description:
* @Date: 2024/2/4 22:05
*/
@Mapper
public interface UserMapper {
User findByAccount(String account);
}
其他的异常处理类、统一结果返回及前端代码我想大家都会
跨域问题
前后端分离项目中,后台SpringBoot整合SpringSecurity之后如果想要前台能正常访问后台接口需要开启SpringBoot和SpringSecurity的跨域访问
package com.smiao.vegetable.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CrossOriginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("Access-Control-Allow-Credentials", "true");
res.addHeader("Access-Control-Allow-Origin", "*");
res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN ,Authorization ,Refreshtoken");
if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {
response.getWriter().println("ok");
return;
}
chain.doFilter(request, response);
}
}