我对Spring Security + JWT的梳理


目录:

  1. 自定义实现UserDetailsService接口
  2. 自定义实现UserDetails接口
  3. JWTUtil工具类
  4. 自定义JWTFilter实现OncePerRequestFilter接口
  5. loginService
  6. 自定义配置类继承WebSecurityConfigurerAdapter
  7. 授权
  8. 其他的示例
  9. 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

  • 重新配置密码加密和校验方法

    image-20220224103156823

  • 在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接口,访问成功

授权

主要通过SecurityInterceptorSecurityContext中获取权限信息来进行权限验证

开启权限,使用注解@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所标记了,未来这个类将被移除。

image-20220313141841557

其他改动,详见链接。


文章作者: 小小千千
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小小千千 !
评论
  目录