Spring Security 的使用

一、简介 

1.1、Spring Security 相关概念

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.2、核心的过滤器链 

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.3、Spring Security 使用场景 

1.用户登录和认证
可处理用户的身份验证。如表单登录、基本认证、OAuth等。
2.授权和权限管理
可定义安全规则和访问控制,可用注解、表达式或配置文件来声明和管理权限,确保用户只能访问其有权访问的资源。
3.防止跨站点请求伪造(CSRF)
可生成和验证CSRF令牌,防止Web应用程序受到CSRF攻击。可在表单中自动添加CSRF令牌,并验证提交请求中的令牌值。
4.方法级安全性
允许在方法级别对方法进行安全性配置。可用注解或表达式来定义哪些用户有权调用特定方法。
5.记住我功能
允许用户在下次访问时保持登录状态,不要重新输入用户名和密码。
6.单点登录(SSO)
可与其他身份验证和授权提供程序集成,实现单点登录。
7.安全事件和审计日志
可记录安全事件和用户操作,以便进行审计和故障排查。

二、SpringBoot 中基本使用 

2.1、认证步骤

1.创立 UserServiceImpl 类
2.完成 UserDetailsService 接口
3.重写 loadUserByUsername 办法
4.依据用户名校验用户并查询用户相关权限信息(授权)
5.将数据封装成 UserDetails(创立类并完成该接口) 并回来 

2.2、引入依赖、配置文件

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>
<!-- Lombok 插件 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

2.2.1、配置登录页面static/login.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login">
	<div class="checkbox">
	    <label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label>
	</div>
	<div>
		用户名:<input type="text" name="username">
	</div>
	<div>
		密码:<input type="password" name="password">
	</div>
	<div>
		<button type="submit">立即登陆</button>
	</div>
</form>
</body>
</html>

2.2.2、配置信息连接信息

# 开发环境装备
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

2.2.3、建表语句DDL

略 . . . . . . 

2.2.4、UserDetails、UserDetailsService的实现

@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<SimpleGrantedAuthority> 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<UserDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserDO::getUsername, username);
        Optional<UserDO> optional = Optional.ofNullable(baseMapper.selectOne(queryWrapper));
        return optional.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
    }
}

2.3、登录认证

2.3.1、用户登录认证 

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<TokenVO> 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);
    }
}

2.3.2、过滤器认证 、退出登录

过滤器认证

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();
    }
}

2.4、认证结果处理

/**
 * 认证——成功处理
 */
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();
	}
}

2.5、授权结果处理

/**
 * 授权失败——定义认证检查失败处理
 */
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<String,Object> result = new HashMap<>();
		result.put("success",false);
		result.put("message","登录失败,用户名或密码错误["+e.getMessage()+"]");
		httpServletResponse.getWriter().print(JSONUtils.toJSONString(result));
	}
}

2.7、获取密码

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));
	}
}
  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值