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-jpadatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/你的数据库?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghaiusername: 你的账号password: 你的密码type: com.alibaba.druid.pool.DruidDataSourcedruid:# 下面为连接池的补充设置,应用到上面所有数据源中# 初始化大小,最小,最大initial-size: 5min-idle: 5max-active: 20jpa:database: MYSQLdatabase-platform: org.hibernate.dialect.MySQL5InnoDBDialectshow-sql: trueopen-in-view: truehibernate:ddl-auto: updatenaming:physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImplproperties:hibernate:enable_lazy_load_no_trans: trueredis:host: 127.0.0.1port: 6379password: 你的密码(没有不填)lettuce:pool:# 最大活动数量max-active: 8# 当池耗尽时,在引发异常之前,连接分配应该阻塞的最长时间。使用负值可以无限期阻止。max-wait: -1# 最大闲置时间,单位:smax-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 认证管理器建造者*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}/*** 授权** @param http 安全*/@Overrideprotected 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();}/*** 加密规则*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 生成一个认证管理器bean* @return* @throws Exception*/@Bean(value = "authenticationManager")@Overridepublic 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;@Overridepublic 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 {@Serialprivate 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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (RoleDto role : userRoles) {authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));}return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return userName;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return !locked;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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");}@Overridepublic 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;}@Overrideprotected 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作为keylog.info("token: " + jwtToken);redisTemplate.opsForValue().set("login-" + userDetail.getId(), userDetail);}@Overrideprotected 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异常*/@Overrideprotected 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);}
}