SpringBoot整合SpringSecurity+JWT
整合SpringSecurity步骤
- 编写拦截链配置类,规定security参数
- 拦截登录请求的参数,对该用户做身份认证。
- 通过登录验证的予以授权,这里根据用户对应的角色作为授权标识。
整合JWT步骤
- 编写JWTUtils,包括生成、验证JWT的方法。
- 编写登录认证过滤器,生成token,并将token中的payload添加到redis中
- 编写路由过滤器,可行的路由则放行
- 登录认证后生成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);
}
}