目录:
- 自定义实现
UserDetailsService
接口- 自定义实现
UserDetails
接口JWTUtil
工具类- 自定义
JWTFilter
实现OncePerRequestFilter
接口loginService
- 自定义配置类继承
WebSecurityConfigurerAdapter
类- 授权
- 其他的示例
- Sping Security的新版本改动
自定义实现UserDetailsService
接口
重写loadUserByUsername
方法,方法返回UserDetails
对象
从数据库查询用户信息进行封装(之前默认的接口是从内存session中获取)
自定义实现UserDetails
接口
自定义实体类字段
重写认证方法
JWTUtil
工具类
提供生成jwt、解析jwt的方法
自定义JWTFilter
实现OncePerRequestFilter
接口
- 从request中获取并解析jwt
- 从Redis中读取用户信息
- 获得
UsernamePasswordAuthenticationToken
类型的authentication
对象 - 将验证信息存入
SecurityContext
上下文SecurityContextHolder.getContext().setAuthentication(authentication);
loginService
获得
UsernamePasswordAuthenticationToken
类型的authentication
对象调用
authenticationManager.authenticate(authentication)
方法进行验证,会调用上面自定义实现的UserDetailsServiceImpl
类的loadUserByUsername
方法验证失败直接抛出异常(
ExceptionTranslationFilter
会捕捉);验证成功就生成JWT并返回。认证成功后,将用户信息存入redis,以便后续JWTFilter解析时候获取用户信息,封装到authentication对象中。
自定义配置类继承WebSecurityConfigurerAdapter
类
重新配置密码加密和校验方法
在Spring容器中暴露
authenticationManager
,用于进行认证将自定义的JWTFilter加入到过滤器链中,指定位置在
UsernamePasswordFilter
面前
注意:在高版本的Spring Security(SpringBoot 2.7.0版本后,对应的Spring Security版本5.7.0后)废除了Web
类,配置的方式也发生了很大的变化,详见:Spring Security配置类将被废除、全新版本Spring Security,这样用才够优雅!
高版本主要改动就是Spring Security配置类,对其他类的使用方法影响不大。
/**
* SpringSecurity 5.4.x以上新用法配置
* 为避免循环依赖,仅用于配置HttpSecurity
* Created by macro on 2022/5/19.
*/
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
//省略HttpSecurity的配置
return httpSecurity.build();
}
}
示例Demo
依赖
<!-- Spring Security -->
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--fastjson依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>4.9</version>
</dependency>
<!-- Spring data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
配置文件
server:
port: 80
context-path: /
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: qianhao123
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
ddl-auto: update #自动更新
show-sql: true #日志中显示sql语句
实体类
数据库实体类
@Data @NoArgsConstructor @AllArgsConstructor @Entity public class User implements Serializable{ private static final long serialVersionUID = 1L; @Id private int id; private String username; private String password; private int age; }
UserDetails的实现类
@Data @AllArgsConstructor @NoArgsConstructor public class UserDetailsImpl implements UserDetails{ private static final long serialVersionUID = 1L; private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { // TODO Auto-generated method stub return null; } @Override public String getPassword() { // TODO Auto-generated method stub return user.getPassword(); } @Override public String getUsername() { // TODO Auto-generated method stub return user.getUsername(); } @Override public boolean isAccountNonExpired() { // TODO Auto-generated method stub return true; } @Override public boolean isAccountNonLocked() { // TODO Auto-generated method stub return true; } @Override public boolean isCredentialsNonExpired() { // TODO Auto-generated method stub return true; } @Override public boolean isEnabled() { // TODO Auto-generated method stub return true; } }
Dao层
public interface UserRepository extends JpaRepository<User, Integer>{
User findByUsername(String username);
User findById(int id);
}
UserDetailService实现类
@Component
public class UserDetailsServiceImpl implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
User user = userRepository.findByUsername(username);
if(user == null) {
throw new RuntimeException("用户名或密码错误");
}
return new UserDetailsImpl(user);
}
}
JWT配置
工具类
public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "jwtkey"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @param ttlMillis token超时时间 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("sg") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
JWT过滤器
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserRepository userRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { //放行 filterChain.doFilter(request, response); return; } //解析token String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //获取用户信息 Optional<User> findById = userRepository.findById(Integer.valueOf(userid)); User user = findById.get(); if(Objects.isNull(user)){ throw new RuntimeException("用户未登录"); } //存入SecurityContextHolder //TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
SecurityConfig配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf
.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Service层
@Service
public class UserService {
@Autowired
private AuthenticationManager authenticationManager;
/**
* 用户登录
* @param user
* @return
*/
public String login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
//使用id生成token
UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal();
String userId = String.valueOf(loginUser.getUser().getId());
String jwt = JwtUtil.createJWT(userId);
//把token响应给前端
return jwt;
}
}
Controller层
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public String login(User user) {
String jwt = userService.login(user);
return jwt;
}
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
单元测试插入用户信息
@SpringBootTest
class SpringSecurityDmeoApplicationTests {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void addUser(){
User user = new User();
user.setUsername("jason");
user.setPassword(passwordEncoder.encode("123456"));
user.setAge(24);
userRepository.save(user);
}
}
主类启动
@SpringBootApplication
public class SpringSecurityDmeoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityDmeoApplication.class, args);
}
}
测试
访问
/login
,获取jwt
直接访问
hello
接口,无应答带着上述
jwt
的请求头,访问hello
接口,访问成功
授权
主要通过SecurityInterceptor
从SecurityContext
中获取权限信息来进行权限验证
开启权限,使用注解@EnableGlobalMethodSecurity(prePostEnabled = true)
在需要权限验证的方法前使用@PreAuthorize
//关于
@PreAuthorize
注解的使用@PreAuthorize("@ss.hasPermi('system:dict:list')") @GetMapping("/list")
请求 /system/dict/data/list 接口,会调用 PermissionService 的 #hasPermi(String permission) 方法,校验用户是否有指定的权限。
为什么这里会有一个 @ss 呢?在 Spring EL 表达式中,调用指定 Bean 名字的方法时,使用 @ + Bean 的名字。在 RuoYi-Vue 中,声明 PermissionService 的 Bean 名字为 ss 。
其他的示例
基本满足上述流程的示例:
还包含对其他接口实现的示例:
一些概念和流程的总结的示例:
Sping Security的新版本改动
详情:https://mp.weixin.qq.com/s/qK-gYDChxxtdFjnIo_ofqw
特别是,WebSecurityConfigurerAdapter
总管Spring Security的配置体系,但是马上这个类要废了,你没有看错,这个类将在5.7版本被@Deprecated
所标记了,未来这个类将被移除。
其他改动,详见链接。