SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】
SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发
1 项目准备
- 创建SpringBoot基础项目
- SpringBoot项目集成mybatis
- SpringBoot 集成 Druid 数据源【SpringBoot系列3】
- SpringBoot MyBatis 实现分页查询数据【SpringBoot系列4】
- SpringBoot MyBatis-Plus 集成 【SpringBoot系列5】
- SpringBoot mybatis-plus-generator 代码生成器 【SpringBoot系列6】
- SpringBoot MyBatis-Plus 分页查询 【SpringBoot系列7】
- # SpringBoot 集成Redis缓存 以及实现基本的数据缓存【SpringBoot系列8】
Spring Security(官网在这里) 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架。
本文章实现的是SpringBoot整合Spring Security实现认证校验功能,实现方式有多种,本文章只是其中一种,如有不足,欢迎留言。
首先在项目的 pom.xml 添加依赖如下:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.0.4</version>
</dependency>
JWT(JSON Web Token) 用来生成 Token ,JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名.
添加依赖后,启动项目,在浏览器中访问任何一个接口都会出现 登录认证
1 jwt 生成 token 工具
这里就是根据 用户的 username + 密钥来生成token ,然后解密 token 等等,在 Spring Security 认证过程中使用。
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
@Slf4j
public class JWTGenerator {
//密钥
private static String sign ="cuAihCz53DZRjZwbsGcZJ2Ai6At+T142uphtJMsk7iQ=";
//生成token
public String generateToken(Authentication authentication) {
//用户的核心标识
String username = authentication.getName();
// 过期时间 - 30分钟
Date expireDate = new Date(System.currentTimeMillis() + 30 * 60 * 1000);
String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(generalKeyByDecoders()) //设置token加密方式和密
.compact();
return token;
}
public static SecretKey generalKeyByDecoders() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(sign));
}
/**
* 解密token
* @param token
* @return
*/
public String getUsernameFromJWT(String token) {
JwtParserBuilder builder = Jwts.parserBuilder();
Jws<Claims> claimsJws = builder
.setSigningKey(generalKeyByDecoders())
.build()
.parseClaimsJws(token);
return claimsJws.getBody().getSubject();
}
/**
* 校验token
* @param token
* @return
*/
public boolean validateToken(String token) {
log.error("验证 token {}", token);
try {
JwtParserBuilder builder = Jwts.parserBuilder();
Jws<Claims> claimsJws = builder
.setSigningKey(generalKeyByDecoders())
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
Claims claims = e.getClaims();
// 检查token
throw new BadCredentialsException("TOKEN已过期,请重新登录!");
} catch (AuthenticationException e) {
throw new AuthenticationCredentialsNotFoundException("JWT was expired or incorrect");
} catch (Exception ex) {
log.error("token认证失败 {}", ex.getMessage());
throw new AuthenticationCredentialsNotFoundException("JWT was expired or incorrect");
}
}
}
2 登录认证 Controller 定义
@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JWTGenerator jwtGenerator;
@PostMapping("login")
public R login(@RequestBody LoginRequest loginDto){
log.info("登录认证开始 {}",loginDto.toString());
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginDto.getUserName(),
loginDto.getPassword()));
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("登录认证完成 {}",loginDto.toString());
String token = jwtGenerator.generateToken(authentication);
log.info("登录认证生成 token {}",token);
return R.okData(token);
}
}
-
JWTGenerator 第一步中定义的 token 生成工具,在登录校验完成时,生成token.
-
AuthenticationManager 只关注认证成功与否而并不关心具体的认证方式,如果验证成功,则返回完全填充的Authentication对象(包括授予的权限)。
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@Data
@ToString
public class LoginRequest implements Serializable {
private String userName ;
private String password;
}
3 核心配置 SecurityConfig
SecurityConfig 用来配置 Spring Security 的拦截策略以及认证策略等等
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//自定义异常认证处理
private JwtAuthEntryPoint authEntryPoint;
//自定义授权异常处理
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
public SecurityConfig(JwtAuthEntryPoint authEntryPoint, MyAccessDeniedHandler myAccessDeniedHandler) {
this.authEntryPoint = authEntryPoint;
this.myAccessDeniedHandler = myAccessDeniedHandler;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler)
.authenticationEntryPoint(authEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//放行静态资源文件夹(路径要具体情况具体分析)
.antMatchers(
"/api/auth/**",
"/css/**", "/js/**", "/image/**",
"/app/**",
"/swagger/**",
"/swagger-ui.html",
"/app/**",
"/swagger-resources/**",
"/v2/**",
"/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 自定义 认证过滤器
@Bean
public JWTAuthenticationFilter jwtAuthenticationFilter() {
return new JWTAuthenticationFilter();
}
}
3.1 JwtAuthEntryPoint 自定义的认证失败的回调处理
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String message = authException.getMessage();
log.error("token 拦截 {}",message);
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
Map<String,Object> map = new HashMap<>();
map.put("code",403);
map.put("message","您未登录,没有访问权限");
response.getWriter().print(JSONObject.toJSONString(map));
}
}
3.2 MyAccessDeniedHandler 自定义的授权失败的回调处理
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 授权异常
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException, IOException {
response.setStatus(403);
response.getWriter().write("Forbidden:" + accessDeniedException.getMessage());
}
}
4 核心过滤器 JWTAuthenticationFilter
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JWTGenerator tokenGenerator;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的 token 信息
String token = getJWTFromRequest(request);
//校验token
if(StringUtils.hasText(token) && tokenGenerator.validateToken(token)) {
//解析 token 中的用户信息 (用户的唯一标识 )
String username = tokenGenerator.getUsernameFromJWT(token);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
/**
* 就是校验请求头的一种格式 可以随便定义
* 只要可以解析 就可以
* @param request
* @return
*/
private String getJWTFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
5 CustomUserDetailsService 用户校验实现
@Service
public class CustomUserDetailsService implements UserDetailsService {
private UserService userService;
@Autowired
public CustomUserDetailsService(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo user = userService.getByUsername(username);
if(user==null){
throw new UsernameNotFoundException("Username not found");
}
User user1 = new User(user.getUserName(), user.getPassword(), mapRolesToAuthorities(user.getRoles()));
return user1;
}
private Collection<GrantedAuthority> mapRolesToAuthorities(List<Role> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
}
这里使用到的 UserService 就是项目中查询用户信息的服务。 然后使用 postman 来访问接口
然后调用 登录接口生成 token
然后在访问其他接口里放入请求头信息
项目源码在这里 :gitee.com/android.lon… 有兴趣可以关注一下公众号:biglead
转载自:https://juejin.cn/post/7215478156949422136