SpringBoot整合SpringSecurity+JWT

SpringBoot整合SpringSecurity+JWT

整合SpringSecurity步骤

  1. 编写拦截链配置类,规定security参数
  2. 拦截登录请求的参数,对该用户做身份认证。
  3. 通过登录验证的予以授权,这里根据用户对应的角色作为授权标识。

整合JWT步骤

  1. 编写JWTUtils,包括生成、验证JWT的方法。
  2. 编写登录认证过滤器,生成token,并将token中的payload添加到redis中
  3. 编写路由过滤器,可行的路由则放行
  4. 登录认证后生成token返回response

结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

依赖Jar

<!-- JWT-->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>
<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-core</artifactId>
	<version>9.0.63</version>
</dependency>
<!--Redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<version>2.6.8</version>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.7.1</version>
</dependency>
<!--Spring Security-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
	<version>2.6.8</version>
</dependency>
<!--Spring data jpa-->
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-jpa</artifactId>
	<version>2.6.4</version>
</dependency>
<!-- querydsl -->
<dependency>
	<groupId>com.querydsl</groupId>
	<artifactId>querydsl-jpa</artifactId>
	<version>5.0.0</version>
</dependency>
<!-- Hibernate对jpa的支持包 -->
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-entitymanager</artifactId>
	<version>5.6.9.Final</version>
</dependency>
<!-- MySQL-->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.13</version>
</dependency>
<!-- Druid-->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
	<version>1.1.16</version>
</dependency>
<!--Spring Boot相关-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>2.6.8</version>
</dependency>
<!--Spring aspect Auditor审计功能需要-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>5.3.20</version>
</dependency>
<!--Hutool 快速开发工具包-->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.9</version>
</dependency>

配置文件yml

server:
  port: 8642
spring:
  application:
	name: spring-data-jpa
  datasource:
	driver-class-name: com.mysql.cj.jdbc.Driver
	url: jdbc:mysql://localhost:3306/你的数据库?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
	username: 你的账号
	password: 你的密码
	type: com.alibaba.druid.pool.DruidDataSource
	druid:
	  # 下面为连接池的补充设置,应用到上面所有数据源中
	  # 初始化大小,最小,最大
	  initial-size: 5
	  min-idle: 5
	  max-active: 20
  jpa:
	database: MYSQL
	database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
	show-sql: true
	open-in-view: true
	hibernate:
	  ddl-auto: update
	  naming:
		physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
	properties:
	  hibernate:
		enable_lazy_load_no_trans: true
  redis:
	host: 127.0.0.1
	port: 6379
	password: 你的密码(没有不填)
	lettuce:
	  pool:
		# 最大活动数量
		max-active: 8
		# 当池耗尽时,在引发异常之前,连接分配应该阻塞的最长时间。使用负值可以无限期阻止。
		max-wait: -1
		# 最大闲置时间,单位:s
		max-idle: 500
	  # 超时关闭时间
	  shutdown-timeout: 0

整合SpringSecurity

Spring Security权限配置类

/**
 * @author Evad.Wu
 * @Description SpringSecurity权限配置类
 * @date 2022-06-28
 */
@Configuration
public class EvadSecurityConfig extends WebSecurityConfigurerAdapter {
	private AuthenticationManager authenticationManager;

	@Resource(name = "evadRedisTemplate")
	private RedisTemplate<String, Object> redisTemplate;
	@Resource(name = "userServiceImpl")
	private BaseUserDetailsService userDetailsService;

	/**
	 * 认证
	 *
	 * @param auth 认证管理器建造者
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService);
	}

	/**
	 * 授权
	 *
	 * @param http 安全
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 开启 HttpSecurity 配置
		http.authorizeRequests()
				.antMatchers("/securityController/login").permitAll()
				.antMatchers("/securityController/evadLogin").permitAll()
				.antMatchers("/securityController/evadLogout").permitAll()
				.antMatchers("/securityController/user").permitAll()
				.antMatchers("/securityController/dev").access("hasAnyRole('DEV','MASTER')")
				.antMatchers("/securityController/devAndUser").access("hasAnyRole('MASTER') or (hasRole('DEV') and hasRole('USER'))")
				.antMatchers("/securityController/master").access("hasAnyRole('MASTER')")
				// 用户访问其它URL都必须认证后访问(登录后访问)
				.anyRequest().authenticated()
				// 开启表单登录并配置登录接口
				.and()
				.formLogin().loginProcessingUrl("/login").permitAll()
				.and()
				.logout().logoutUrl("/logout")
				.addLogoutHandler(new EvadLogoutHandler())
				.invalidateHttpSession(true)
				.deleteCookies("JSESSIONID", "XXL_JOB_LOGIN_IDENTITY")
				.clearAuthentication(true)
				.logoutSuccessUrl("/login")
				.permitAll()
				.and().exceptionHandling()
				.accessDeniedHandler((request, response, e) -> {
					request.setAttribute("state", 403);
					request.setAttribute("errMsg", "抱歉,您没有权限访问!");
					request.getRequestDispatcher("/toErrorPage");
				})
				// 添加jwt验证
				.and()
				.addFilter(new JwtLoginFilter(authenticationManager, redisTemplate))
				.addFilter(new JwtValidationFilter(authenticationManager, redisTemplate))
				// 不使用HttpSession
				.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
				.and()
				.cors()
				.and().csrf().disable();
	}

	/**
	 * 加密规则
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	/**
	 * 生成一个认证管理器bean
	 * @return
	 * @throws Exception
	 */
	@Bean(value = "authenticationManager")
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		this.authenticationManager = super.authenticationManagerBean();
		return authenticationManager;
	}
}

校验登录信息(继承UserDetailsService接口),并授权(根据roles)

/**
 * @Description 用户认证信息的顶级接口
 * @author Evad.Wu
 * @date 2022-06-28
 */
public interface BaseUserDetailsService extends UserDetailsService {
}


/**
 * @author Evad.Wu
 * @Description 登录时校验数据库中的密码
 * @date 2022-06-28
 */
@Service
public class UserServiceImpl implements BaseUserDetailsService {
	@Resource(name = "userRepository")
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
		User user = Optional.ofNullable(userRepository.findFirstByUsername(s)).orElseGet(User::new);
		if (user.getPassword().isEmpty()) {
			throw new UsernameNotFoundException("用户不存在!");
		}
		return this.user2UserDetail(user);
	}

	private UserDetail user2UserDetail(User user) {
		UserDetail userDetail = new UserDetail();
		userDetail.setId(user.getId());
		userDetail.setPassword(user.getPassword());
		userDetail.setUserName(user.getUsername());
		userDetail.setUserRoles(this.role2Dto(user.getRoles()));
		Boolean visible = Optional.ofNullable(user.getVisible()).orElse(true);
		userDetail.setEnabled(visible);
		userDetail.setLocked(!visible);
		return userDetail;
	}

	private List<RoleDto> role2Dto(Set<Role> roleList) {
		List<RoleDto> roleDtolist = new ArrayList<>();
		for (Role role : roleList) {
			RoleDto roleDto = new RoleDto();
			roleDto.setId(role.getId());
			roleDto.setRoleName(role.getRoleName());
			roleDto.setRoleCode(role.getRoleCode());
			roleDtolist.add(roleDto);
		}
		return roleDtolist;
	}
}

UserDetail 校验登录信息的对象(实现UserDetails)

/**
 * @author Evad.Wu
 * @Description 用户信息转换类
 * @date 2022-06-28
 */
@Data
@NoArgsConstructor
@JsonIgnoreProperties({"username", "password", "enabled", "accountNonExpired", "accountNonLocked", "credentialsNonExpired", "authorities"})
public class UserDetail implements UserDetails {
	@Serial
	private static final long serialVersionUID = -2028119927623038905L;

	private Long id;

	private String userName;

	private String password;

	private Boolean enabled;

	private Boolean locked;

	private List<RoleDto> userRoles;

	private List<SimpleGrantedAuthority> authorities;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		for (RoleDto role : userRoles) {
			authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
		}
		return authorities;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {
		return userName;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return !locked;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return enabled;
	}
}

整合JWT

JWTUtils工具类

注意:SIGNATURE是生成token的公钥,当外部token进来时需要公钥解密。

/**
 * @author Evad.Wu
 * @Description JWT 工具类
 * @date 2023-01-15
 */
public class JWTUtils {
	/**
	 * 生成token
	 */
	public static <T extends UserDetails> String createToken(T principal, Long expire) {
		JwtBuilder jwtBuilder = Jwts.builder();
		Map<String, Object> headerParams = new HashMap<>(16);
		headerParams.put("typ", "JWT");
		headerParams.put("alg", SignatureAlgorithm.HS256.getValue());
		Map<String, Object> claims = new HashMap<>(16);
		UserDetail user = (UserDetail) principal;
		claims.put("id", user.getId());
		claims.put("username", principal.getUsername());
		claims.put("role", user.getUserRoles());
		Date exp = new Date(System.currentTimeMillis() + expire);
		claims.put("exp", exp);
		return jwtBuilder
				.setHeader(headerParams)
				.setIssuer(principal.getUsername())
				.setIssuedAt(new Date())
				.setClaims(claims)
				.setExpiration(exp)
				.signWith(SignatureAlgorithm.HS256, EvadSecretConstant.SIGNATURE)
				.compact();
	}

	/**
	 * 解析token
	 *
	 * @param token 令牌
	 * @return 解析结果
	 */
	public static boolean checkToken(String token) {
		JwtParser jwtParser = Jwts.parser();
		jwtParser.setSigningKey(EvadSecretConstant.SIGNATURE);
		try {
			jwtParser.parse(token);
			return true;
		} catch (ExpiredJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * 解析token
	 *
	 * @param token     令牌
	 * @param sercetKey 用户认证秘钥
	 * @return 用户认证信息参数
	 */
	public static Claims verifyToken(String token, String sercetKey) {
		return Jwts.parser()
				.setSigningKey(DatatypeConverter.parseBase64Binary(sercetKey))
				.parseClaimsJws(token).getBody();
	}
}

JWT登录认证过滤器

/**
 * @author Evad.Wu
 * @Description jwt用户信息认证 过滤器
 * @date 2023-01-16
 */
@Slf4j
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
	/**
	 * 获取授权管理
	 */
	private final AuthenticationManager authenticationManager;
	private final RedisTemplate<String, Object> redisTemplate;

	public JwtLoginFilter(AuthenticationManager authenticationManager, RedisTemplate<String, Object> redisTemplate) {
		this.authenticationManager = authenticationManager;
		this.redisTemplate = redisTemplate;
		// 指定一个路由作为登录认证的入口
		super.setFilterProcessesUrl("/securityController/evadLogin");
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		Authentication authentication;
		try {
			BufferedReader br = request.getReader();
			StringBuilder body = new StringBuilder();
			String str;
			while ((str = br.readLine()) != null) {
				body.append(str);
			}
			LoginVo loginVo = JSONObject.parseObject(body.toString(), LoginVo.class);
			//先得到前端传入的账号密码Authentication对象
			UsernamePasswordAuthenticationToken authenticationToken =
					new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
			//AuthenticationManager authentication进行用户认证
			authentication = authenticationManager.authenticate(authenticationToken);
			System.out.println("authencation: " + authentication);
			if (Optional.ofNullable(authentication).isEmpty()) {
				response.setCharacterEncoding("UTF-8");
				response.getWriter().print("登录失败!");
				return null;
			}
			return authentication;
		} catch (IOException e) {
			logger.error(e.getMessage());
		}
		return null;
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
		UserDetail userDetail = (UserDetail) authResult.getPrincipal();
		String jwtToken = JWTUtils.createToken(userDetail, 30 * 60 * 1000L);
		response.addHeader("token", jwtToken);
		//把完整的用户信息存入redis userid作为key
		log.info("token: " + jwtToken);
		redisTemplate.opsForValue().set("login-" + userDetail.getId(), userDetail);
	}

	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
		response.setCharacterEncoding("UTF-8");
		response.getWriter().print("登录失败!");
	}
}

JWT token校验过滤器

/**
 * @author Evad.Wu
 * @Description jwt验证令牌 过滤器
 * @date 2023-01-16
 */
@Slf4j
public class JwtValidationFilter extends BasicAuthenticationFilter {
	private final RedisTemplate<String, Object> redisTemplate;

	public JwtValidationFilter(AuthenticationManager authenticationManager, RedisTemplate<String, Object> redisTemplate) {
		super(authenticationManager);
		this.redisTemplate = redisTemplate;
	}

	/**
	 * 过滤请求验证
	 *
	 * @param request     请求体
	 * @param response    响应体
	 * @param filterChain 请求过滤链
	 * @throws IOException      IO异常
	 * @throws ServletException servlet异常
	 */
	@Override
	protected void doFilterInternal(
			@NonNull HttpServletRequest request,
			@NonNull HttpServletResponse response,
			@NonNull FilterChain filterChain) throws ServletException, IOException {
		String token = request.getHeader("token");
		if (Optional.ofNullable(token).isEmpty()) {
			filterChain.doFilter(request, response);
			return;
		}
		response.setHeader("token", token);
		Claims claims = JWTUtils.verifyToken(token, EvadSecretConstant.SIGNATURE);
		Long id = claims.get("id", Long.class);
		Date exp = claims.getExpiration();
		UserDetail userDetail = (UserDetail) redisTemplate.opsForValue().get("login-" + id);
		System.out.println("过期时间:" + exp);
		log.info("解析到的用户: " + userDetail);
		if (Optional.ofNullable(userDetail).isEmpty()) {
			throw new RuntimeException("用户未登录");
		}
		// 存入SecurityContextHolder, 其他filter会通过这个来获取当前用户信息
		// 获取权限信息封装到authentication中
		UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
				= new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
		SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
		// 放行
		filterChain.doFilter(request, response);
	}
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加把劲骑士RideOn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值