# SpringSecurity2023 **Repository Path**: J_M_Hao/spring-security2023 ## Basic Information - **Project Name**: SpringSecurity2023 - **Description**: SpringBoot + Spring Security + JWT - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-12-11 - **Last Updated**: 2023-12-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Spring Security ## Spring Security概述 Spring Security是Spring框架的一个模块,用于提供身份验证(Authentication)、授权(Authorization)和其他安全功能的支持。它是一个强大且可定制的框架,用于保护Spring应用程序中的资源。 主要包括有以下特点: 1. **身份验证(Authentication):** - Spring Security提供了多种身份验证机制,包括基于用户名和密码的认证、基于令牌的认证、LDAP认证等。 - 支持自定义身份验证提供者,使开发者能够集成各种认证方式。 2. **授权(Authorization):** - Spring Security支持基于角色和权限的访问控制,可以定义哪些用户具有哪些角色和权限。 - 提供注解和XML配置两种方式来声明安全性规则。 3. **Web安全性:** - 针对Web应用程序,Spring Security提供了一套完整的安全性特性,包括保护URL、CSRF防护、Session管理等。 - 支持基于表达式的访问控制,可以在方法级别和URL级别定义安全规则。 4. **方法级安全性:** - 允许在方法级别定义安全性规则,确保只有具有适当权限的用户能够执行受保护的方法。 5. **记住我(Remember Me):** - 提供"记住我"功能,使用户在注销后能够通过记住的令牌重新登录。 6. **单点登录(Single Sign-On,SSO):** - Spring Security可以与其他单点登录解决方案(如OAuth、CAS等)集成,实现跨应用程序的单一身份认证。 7. **集成:** - 可以轻松集成到Spring应用程序中,与Spring框架的其他模块无缝合作。 8. **可扩展性:** - 允许开发者根据应用程序的需求进行自定义,实现特定的安全性逻辑。 9. **事件驱动:** - Spring Security基于事件驱动的模型,允许开发者在安全事件发生时进行监听和处理。 官网(https://spring.io/projects/spring-security) ## 使用 ### 引入依赖 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security ``` Security 通常都是通过自定义`UserDetailsService`, `AuthenticationProvider`, `AuthenticationManager`,`UsernamePasswordAuthenticationFilter`其中的一种来实现的。 最常见的是通过`UserDetailsService`结合`UsernamePasswordAuthenticationFilter`方式来实现 ### 创建`UserDetails`实体 在`Spring Security`中`UserDetails`接口包含了有关用户的基本信息(用户名、密码、权限、是否锁定等)。我们可以结合此接口以及用户表来扩展我们的实体类。 ```java package com.kfm.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.kfm.util.GrantedAuthorityDeserializer; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; import java.io.Serializable; import java.util.Date; import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Data @Entity @Table(name = "sys_user") public class SysUser implements Serializable, UserDetails { @Id @Column(name = "id", nullable = false, length = 32, precision = 0) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", length = 20, precision = 1) private String username; @Column(name = "password", nullable = false, length = 60, precision = 2) private String password; @Column(name = "create_time") private Date createTime; @Column(name = "update_time", precision = 4) private Date updateTime; @Column(name = "state", precision = 5) private Integer state; /** * JPA 新增时自动设置创建时间 */ @PrePersist public void prePersist() { this.createTime = new Date(); this.state = 1; } /** * JPA 更新时自动设置更新时间 */ @PreUpdate public void preUpdate() { this.updateTime = new Date(); } /** * authorities 是一个权限集合,可以用于存储用户的权限信息 * 此字段不是数据库字段,用于封装用户权限 * getAuthorities() 方法返回用户的所有权限 * * @JsonDeserialize(using = GrantedAuthorityDeserializer.class) * 由于 SimpleGrantedAuthority 没有无参构造函数,所以需要自定义反序列化器 */ @Transient @JsonDeserialize(using = GrantedAuthorityDeserializer.class) private Set authorities; @Transient private boolean accountNonExpired; @Transient private boolean accountNonLocked; @Transient private boolean credentialsNonExpired; @Transient private boolean enabled; /** * 用户的账号是否未过期 * @return true 表示账号未过期 */ public boolean isAccountNonExpired() { return this.state == 1; } /** * 用户的账号是否未锁定 * @return true 账号未锁定 */ public boolean isAccountNonLocked() { return this.state == 1; } /** * 用户的密码是否未过期 * @return true 密码未过期 */ public boolean isCredentialsNonExpired() { return this.state == 1; } /** * 用户是否可用 * @return true 账号状态可用 */ public boolean isEnabled() { return this.state == 1; } } ``` > `UserDetails`接口中的抽象方法都是通过`@Data`注解重写的,所以此处未看到重写接口中的抽象方法 ### 实现UserDetailsService接口 `UserDetailsService`接口负责加载用户信息,在`loadUserByUsername`方法中,你可以从数据库、外部服务等地方获取用户信息。我们可以在`SysUserService`(用户表的Service)接口中继承此接口。 ```java public interface SysUserService extends UserDetailsService {} ``` **实现类** ```java package com.kfm.service.impl; import com.kfm.dao.SysUserRepository; import com.kfm.domain.SysUser; import com.kfm.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; import java.util.HashSet; import java.util.Set; @Service public class SysUserServiceImpl implements SysUserService { @Autowired private SysUserRepository sysUserRepository; /** * 根据用户名获取用户信息 * @param username 用户名 * @return 用户信息 * @throws UsernameNotFoundException 用户名不存在异常 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserRepository.findByUsername(username); if (ObjectUtils.isEmpty(sysUser)){ throw new UsernameNotFoundException("用户名不存在"); } // TODO 此处应该将该用户权限查询出来, 我们后续再改造,现在先默认 Set authorities = new HashSet<>(); // 设置角色权限 authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 设置资源权限 authorities.add(new SimpleGrantedAuthority("sys:user:add")); sysUser.setAuthorities(authorities); return sysUser; } } ``` ### 自定义登录流程 ```java package com.kfm.security; import com.fasterxml.jackson.databind.json.JsonMapper; import com.kfm.util.JwtUtil; import com.kfm.util.Resp; import com.kfm.util.SecurityUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.util.StringUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定义登录流程 * 该过滤器继承自 UsernamePasswordAuthenticationFilter * UsernamePasswordAuthenticationFilter 是 Spring Security 提供的默认登录过滤器 */ @Slf4j // 日志注解 public class SecurityLoginFilter extends UsernamePasswordAuthenticationFilter { public SecurityLoginFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 从请求中获取用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); if (!StringUtils.hasText(username) || !StringUtils.hasText(password)){ throw new RuntimeException("用户名或密码不能为空"); } // 调用封装的工具类进行登录 SecurityUtil.login(username, password, getAuthenticationManager()); return SecurityUtil.getAuthentication(); } /** * 重写 successfulAuthentication 方法 * 该方法会在认证成功后调用 * @param request * @param response * @param chain * @param authResult the object returned from the attemptAuthentication * method. * @throws IOException * @throws ServletException */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { Object principal = authResult.getPrincipal(); log.info("principal: {}", principal); JsonMapper jsonMapper = new JsonMapper(); // 生成 jwt String jwt = JwtUtil.sign(jsonMapper.writeValueAsString(principal)); response.setHeader("Authorization", jwt); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(jsonMapper.writeValueAsString(Resp.ok(jwt))); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { log.error("登录失败, {}", e); Resp result; if (e instanceof AccountExpiredException) { //账号过期 result = Resp.error("账号过期"); } else if (e instanceof BadCredentialsException) { //密码错误 result = Resp.error("密码错误"); } else if (e instanceof CredentialsExpiredException) { //密码过期 result = Resp.error("密码过期"); } else if (e instanceof DisabledException) { //账号不可用 result = Resp.error("账号不可用"); } else if (e instanceof LockedException) { //账号锁定 result = Resp.error("账号锁定"); } else if (e instanceof InternalAuthenticationServiceException) { //用户不存在 result = Resp.error("用户不存在"); }else{ //其他错误 result = Resp.error("未知异常"); } response.setContentType("application/json;charset=utf-8"); JsonMapper jsonMapper = new JsonMapper(); response.getWriter().write(jsonMapper.writeValueAsString(result)); } } ``` > 此处如果不使用`UsernamePasswordAuthenticationFilter`可以提供一个登录接口(`controller`)。该接口需要在配置当中放行,未授权访问需要授权的请求时,会返回401或者403状态码,前端可以根据这个进行路由提示处理 > > ```java > @RestController > public class LoginController { > > > @Autowired > AuthenticationManager authenticationManager; > > @PostMapping(value = "login") > public ResultInfo login(@RequestBody Map params) { > UserInfo userInfo = SecurityUtils.login(params.get("username"), params.get("password"), authenticationManager); > return ResultInfo.success(userInfo); > } > } > ``` > > 此时,就可以不写这个Filter。也不用在下一节配置此Filter ### 配置 ```java package com.kfm.config; import com.kfm.security.JwtAuthenticationFilter; import com.kfm.security.MyAuthenticationEntryPoint; import com.kfm.security.SecurityLoginFilter; import com.kfm.service.SysUserService; 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.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity // 开启Spring Security @EnableGlobalAuthentication // 开启全局认证 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SysUserService sysUserService; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; /** * 配置密码加密器 * 这里我们使用官方推荐的加密方式 BCryptPasswordEncoder * 如果要自定义密码加密器, 实现 PasswordEncoder 接口即可 * @return */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 配置安全拦截机制 * @param http the {@link HttpSecurity} * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() //用于配置直接放行的请求 .antMatchers("/login").permitAll() //其余请求都需要验证 .anyRequest().authenticated() //授权码模式需要 会弹出默认自带的登录框 .and().httpBasic().disable() //禁用跨站伪造 .csrf().disable() // 禁用 session .sessionManagement().disable() // 异常处理 .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint); // 添加 JWT 认证过滤器 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // 自定义登录认证流程 http.addFilterBefore(new SecurityLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); } @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); // 用户认证service - 查询数据库的逻辑 provider.setUserDetailsService(sysUserService); // 设置密码加密算法 provider.setPasswordEncoder(passwordEncoder()); auth.authenticationProvider(provider); } } ``` ### 认证异常处理 ```java /** * 用于处理认证过程中出现的异常。 * 如:未登录访问受保护的资源时,会抛出异常,此时就会调用该类的 commence 方法 */ @Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { Resp result = Resp.error("请先登录"); JsonMapper json = new JsonMapper(); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(json.writeValueAsString(result)); } } ```