1.过滤器链(Filter Chain)
基于Servlet过滤器(Filter)处理和拦截请求,进行身份验证、授权等安全操作。过滤器链按顺序执行,每个过滤器负责一个具体的安全功能。
2.SecurityInterceptor(安全拦截器)
根据配置的安全规则拦截请求,进行访问控制和权限验证。
3.Authentication(认证对象)
封装用户的认证信息(账户状态、用户名、密码、权限等)
Authentication常用实现类:
UsernamePasswordAuthenticationToken:用户名密码登录的 Token
AnonymousAuthenticationToken:针对匿名用户的 Token
RememberMeAuthenticationToken:记住我功能的的 Token
4.AuthenticationManager (用户认证的管理类)
所有的认证请求都会封装成一个Token 给 AuthenticationManager,AuthenticationManager 调用 AuthenticationProvider.authenticate() 认证,返回包含认证信息的 Authentication 对象。
5.AuthenticationProvider(认证的具体实现类)
一个 provider 是一种认证方式实现,主流的认证方式都已经提供了默认实现,如 DAO、LDAP、CAS、OAuth2等。
6.UserDetailService(用户详细信息服务)
通过 UserDetailService 拿到数据库(或内存)中的认证信息然后和客户端提交的认证信息做校验。
7.访问决策管理器(AccessDecisionManager)
在授权过程中进行访问决策。根据用户的认证信息、请求的URL和配置的权限规则,判断用户是否有权访问资源。
8.SecurityContext(安全上下文)
认证通过后,会为这用户生成一个唯一的 SecurityContext(ThreadLocal存储),包认证信息 Authentication。
通过 SecurityContext 可获取到用户的标识 Principle 和授权信息 GrantedAuthrity。
系统任何地方只要通过 SecurityHolder.getSecruityContext() 可获取到 SecurityContext。
9.注解和表达式支持
用在代码中声明和管理安全规则。如@Secured注解可以标记在Controller或方法上,限制权限用户才能访问。
1.SecurityContextPersistenceFilter
Filter的入口和出口,将 SecurityContext (登录后的信息)对象持久到Session,同时把 SecurityContext 设置给 SecurityContextHolder 获取用户认证授权信息。
2.UsernamePasswordAuthenticationFilter
默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息封装成 UsernamePasswordAuthenticationToken,然后调 AuthenticationManager 进行认证。
3.BasicAuthenticationFilter
基本认证,支持 httpBasic 认证方式的Filter。
4.RememberAuthenticationFilter
记住我功能实现的 Filter。
5.AnonymousAuthenticationFilter
处理匿名访问的资源,如果用户未登录,会创建匿名的Token(AnonymousAuthenticationToken),通过 SecurityContextHodler 设置到 SecurityContext 中。
6.ExceptionTranslationFilter
捕获 FilterChain 所有的异常,但只处理 AuthenticationException、AccessDeniedException 异常,其他的异常会继续抛出。
7.FilterSecurityInterceptor
做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用 AccessDecisionManager.decide 方法对用户授权。
1.用户登录和认证
可处理用户的身份验证。如表单登录、基本认证、OAuth等。
2.授权和权限管理
可定义安全规则和访问控制,可用注解、表达式或配置文件来声明和管理权限,确保用户只能访问其有权访问的资源。
3.防止跨站点请求伪造(CSRF)
可生成和验证CSRF令牌,防止Web应用程序受到CSRF攻击。可在表单中自动添加CSRF令牌,并验证提交请求中的令牌值。
4.方法级安全性
允许在方法级别对方法进行安全性配置。可用注解或表达式来定义哪些用户有权调用特定方法。
5.记住我功能
允许用户在下次访问时保持登录状态,不要重新输入用户名和密码。
6.单点登录(SSO)
可与其他身份验证和授权提供程序集成,实现单点登录。
7.安全事件和审计日志
可记录安全事件和用户操作,以便进行审计和故障排查。
1.创立 UserServiceImpl 类
2.完成 UserDetailsService 接口
3.重写 loadUserByUsername 办法
4.依据用户名校验用户并查询用户相关权限信息(授权)
5.将数据封装成 UserDetails(创立类并完成该接口) 并回来
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-data-redis
com.auth0
java-jwt
4.4.0
org.projectlombok
lombok
登陆
登陆
# 开发环境装备
server:
# 服务端口
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security?useUnicode=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF8&nullCatalogMeansCurrent=true
username: "root"
password: "88888888"
redis:
host: 127.0.0.1
port: 6379
database: 0
password: 88888888
security:
# 密钥
secret: spring-boot-learning-examples
# 拜访令牌过期时刻(1天)
access-expires: 86400
# 刷新令牌过期时刻(30天)
refresh-expires: 2592000
# 白名单
white-list: /user/login,/user/register,/user/refresh
略 . . . . . .
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginUser implements UserDetails {
/**
* 用户编号
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 暗码
*/
@JsonIgnore
private String password;
/**
* 权限调集
*/
@JsonIgnore
private List authorities;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
UserDO user = getUserByUsername(username);
// TODO 查询用户权限信息
return LoginUser.builder()
.id(user.getId())
.username(user.getUsername())
.password(user.getPassword())
.build();
}
@Override
public UserDO getUserByUsername(String username) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserDO::getUsername, username);
Optional optional = Optional.ofNullable(baseMapper.selectOne(queryWrapper));
return optional.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
}
}
1.创立 Spring Security 装备类。
2.生成 SecurityFilterChain Bean 办法。
3.放行登录接口。
4.注入 AuthenticationManager 认证管理器。
5.用户认证。
6.生成JWT令牌并回来(双令牌机制)。
7.拜访令牌(AccessToken)存入 Redis 缓存。
/**
* 配置 HttpSecurity
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
/**
* 放行登录接口
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 过滤恳求
.authorizeRequests()
// 接口放行
.antMatchers("/user/login").permitAll()
// 除上面外的一切恳求悉数需求鉴权认证
.anyRequest()
.authenticated()
.and()
// CSRF禁用
.csrf().disable()
// 禁用HTTP呼应标头
.headers().cacheControl().disable()
.and()
// 根据JWT令牌,无需 Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return httpSecurity.build();
}
/**
* 设置加密
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注入 AuthenticationManager 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
/**
* 登录接口
*/
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseVO login(@RequestBody @Validated LoginDTO dto) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()));
UserDO user = userService.getUserByUsername(dto.getUsername());
TokenVO token = JwtUtil.generateTokens(user.getUsername());
redisUtil.set("user:token:" + user.getUsername() + ":string", token.getAccessToken(), JwtUtil.getAccessExpires());
return ResponseVO.success("登录成功", token);
}
}
过滤器认证
1.接口白名单放行。
2.从恳求头中解析令牌。
3.判别令牌是否存在于黑名单中。
4.从 Redis 获取令牌。
5.校验令牌是否合法或有效。
6.存入 SecurityContextHolder。
7.装备过滤器次序。
退出登录
1.全局过滤器中需求判别黑名单是否存在当时拜访令牌
2.解析恳求头中令牌(JTI 与 EXPIRES_AT)
3.将JTI字段作为键存放到 Redis 缓存中,并设置拜访令牌过期时刻
4.清除认证信息
5.装备退出登录接口与处理器
/**
* 配置过滤器
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
if (Arrays.stream(JwtUtil.getWhiteList()).anyMatch(uri -> uri.equals(request.getServletPath()))) {
filterChain.doFilter(request, response);
return;
}
String token = JwtUtil.decodeTokenFromRequest(request);
// 判别令牌是否存在黑名单中
if (redisUtil.hasKey("token:black:" + JwtUtil.getJti(token) + ":string")) {
throw new RuntimeException(Code.TOKEN_INVALID.getZhDescription());
}
String username = JwtUtil.getUsername(token);
if (StringUtils.hasText(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!StringUtils.hasText(redisUtil.get("user:token:" + username + ":string"))) {
throw new RuntimeException(Code.ACCESS_TOKEN_EXPIRED.getZhDescription());
}
// 校验令牌是否有效
try {
JwtUtil.decodeAccessToken(token);
JwtUtil.checkTokenValid(token, userDetails.getUsername());
} catch (TokenExpiredException e) {
// TODO 全局异常处理
throw new RuntimeException(Code.ACCESS_TOKEN_EXPIRED.getZhDescription());
} catch (JWTVerificationException e) {
throw new RuntimeException(Code.TOKEN_INVALID.getZhDescription());
}
// 权限信息
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
/**
* 退出接口
*/
@Service
@RequiredArgsConstructor
public class LogoutHandler implements org.springframework.security.web.authentication.logout.LogoutHandler {
@Autowired
private RedisUtil redisUtil;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = JwtUtil.decodeTokenFromRequest(request);
blacklist(token);
SecurityContextHolder.clearContext();
}
/**
* 参加黑名单
*/
private void blacklist(String token) {
String jti = JwtUtil.getJti(token);
Long expires = JwtUtil.getExpires(token);
redisUtil.set("token:black:" + jti + ":string", StringConstant.EMPTY, DateUtil.minusSeconds(expires));
}
}
/**
* 退出成功接口
*/
@Component
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SecurityContextHolder.clearContext();
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.success()));
response.getWriter().flush();
}
}
/**
* 配置 HttpSecurity
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private LogoutHandler logoutHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 过滤恳求
.authorizeRequests()
// 静态资源放行
.antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
// 接口放行
.antMatchers(JwtUtil.getWhiteList()).permitAll()
// 除上面外的一切恳求悉数需求鉴权认证
.anyRequest()
.authenticated()
.and()
// CSRF禁用
.csrf().disable()
// 禁用HTTP呼应标头
.headers().cacheControl().disable()
.and()
// 根据JWT令牌,无需Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 拦截器
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 退出登录
.logout()
.logoutUrl("/user/logout")
.logoutSuccessHandler(logoutSuccessHandler)
.addLogoutHandler(logoutHandler);
return httpSecurity.build();
}
}
/**
* 认证——成功处理
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>();
map.put("success",true);
map.put("message","认证成功");
map.put("data",authentication);
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
/**
* 认证——失败处理
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>();
map.put("success",false);
map.put("message","认证失败");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
/**
* 授权失败——定义认证检查失败处理
*/
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String result = JSON.toJSONString(AjaxResult.me().setSuccess(false).setMessage("无访问权限"));
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(result);
writer.flush();
writer.close();
}
}
/**
* 授权失败——定义匿名用户访问无权处理
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
e.printStackTrace();
httpServletResponse.setContentType("application/json;charset=utf-8");
Map result = new HashMap<>();
result.put("success",false);
result.put("message","登录失败,用户名或密码错误["+e.getMessage()+"]");
httpServletResponse.getWriter().print(JSONUtils.toJSONString(result));
}
}
public class PasswordTest {
@Test
public void testPassword(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String enPass = bCryptPasswordEncoder.encode("123");
System.out.println(enPass);
System.out.println(bCryptPasswordEncoder.matches("123", enPass));
}
}