SpringSecurityFoundation
- 一. 关于Spring Security
- 二. 关于Spring Security的配置
- 三. WebSecurityConfigurerAdapter接口
- 四. UserDetailsService接口
- 五. JWT
- 六. Spring Security + JWT 实现单点登录(SSO)
- 1. 基本流程
- 八. 登录验证
- 九. JWT 与 雪花ID+Redis 问题
一. 关于Spring Security
- Spring Security框架主要解决了认证与授权相关的问题。
- 认证信息(Authentication):表示用户的身份信息
- 认证(Authenticate):识别用户的身份信息的行为,例如:登录
- 授权(Authorize):授予用户权限,使之可以进行某些访问,反之,如果用户没有得到必要的授权,将无法进行访问
1.1 添加依赖
- 依赖
<!-- Spring Boot支持Spring Security的依赖项,用于处理认证与授权 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.version}</version> </dependency>
1.2 Spring Security框架的典型特征
当添加了spring-boot-starter-security依赖后,在启动项目时执行一些自动配置,具体表现有
1.2.1 所有请求必须登录
- 所有请求(包括根本不存在的)都是必须要登录才允许访问的,如果未登录,会自动跳转到框架自带的登录页面
1.2.2 当尝试登录时
- 当尝试登录时,如果在打开登录页面后重启过服务器端,则第1次的输入是无效的
- 具体原因参见后续的CSRF相关内容
1.2.3 默认的用户名是user
- 默认的用户名是user,密码是在启动项目是控制台提示的一段UUID值,每次启动项目时都不同
- UUID是通过128位算法(运算结果是128个bit)运算得到的,是一个随机数,在同一时空是唯一的,通常使用32个十六进制数来表示,每种平台生成UUID的API和表现可能不同,UUID值的种类有2的128次方个,即:3.4028237e+38,也就是340282366920938463463374607431768211456
1.2.4 当登录成功后,会自动跳转到此前尝试访问的URL
1.2.5 当登录成功后,可以通过 /logout 退出登录
1.2.6 默认不接受普通POST请求
- 默认不接受普通POST请求 如果提交POST请求,将响应403(Forbidden)
- 具体原因参见后续的CSRF相关内容
二. 关于Spring Security的配置
2.1 关闭所有配置
- 关闭所有配置
@Slf4j @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // super.configure(http); // 不保留配置 } }
2.2 默认登录页控制 formLogin()
- 若有此配置则需要权限时弹出默认登录页面而不是403
@Slf4j @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 如果调用以下方法,当Security认为需要通过认证,但实际未通过认证时,就会跳转到登录页面 // 如果未调用以下方法,将会响应403错误 http.formLogin(); } }
2.3 请求的授权访问(访问控制)
- 举例
@Override protected void configure(HttpSecurity http) throws Exception { // 白名单 // 使用1个星号,可以通配此层级的任何资源,例如:/admin/*,可以匹配:/admin/add-new、/admin/list,但不可以匹配:/admin/password/change // 使用2个连续的星可以,可以通配若干层级的资源,例如:/admin/**,可以匹配:/admin/add-new、/admin/password/change String[] urls = { "/doc.html", "/**/*.css", "/**/*.js", "/swagger-resources", "/v2/api-docs", }; // 配置授权访问 // 注意:以下授权访问的配置,是遵循“第一匹配原则”的,即“以最先匹配到的规则为准” // 例如:anyRequest()是匹配任何请求,通常,应该配置在最后,表示“除了以上配置过的以外的所有请求” // 所以,在开发实践中,应该将更具体的请求配置在靠前的位置,将更笼统的请求配置在靠后的位置 http.authorizeRequests() // 开始对请求进行授权 .mvcMatchers(urls) // 匹配某些请求 .permitAll() // 许可,即不需要通过认证就可以访问 .anyRequest() // 任何请求 .authenticated() // 要求已经完成认证的 ; }
2.4 使用自定义账号登录
2.4.1 指定明文密码登录
- 举例
@Slf4j @Service public class UserDetailsServiceImpl implements UserDetailsService { /*** * 指定密码不需要经过加密处理 * @return */ @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { log.debug("用户名:{}", s); // 假设正确的用户名是root,匹配的密码是1234 if (!"root".equals(s)) { log.debug("此用户名没有匹配的用户数据,将返回null"); return null; } log.debug("用户名匹配成功!准备返回此用户名匹配的UserDetails类型的对象"); UserDetails userDetails = User.builder() .username(s) .password("1234") .disabled(false) // 账号状态是否禁用 .accountLocked(false) // 账号状态是否锁定 .accountExpired(false) // 账号状态是否过期 .credentialsExpired(false) // 账号的凭证是否过期 .authorities("这是一个临时使用的山寨的权限!!!") // 权限 .build(); log.debug("即将向Spring Security返回UserDetails类型的对象:{}", userDetails); return userDetails; } }
2.4.2 使用数据库中的账号登录
2.4.2.1 配置数据库账号的密码加密方式为BCryptPasswordEncoder()
- 举例
root $2a$10$VgJZ/AsasCll6SE7WGXsWONCGdpBT8Z2dBSXiNVGOHiJS7GETW6nm
2.4.2.2 编写sql 推算出是要的VO类型
- 举例
select username, password, enable from ams_admin where username=?
2.4.2.3 编写VO类
- pojo.vo.AdminLoginInfoVO
@Data @Accessors(chain = true) public class AdminLoginInfoVO implements Serializable { private String username; private String password; private Integer enable; }
2.4.2.4 编写mapper层
- 举例
@Mapper public interface AdminLoginInfoMapper { //@Select(" select username, password, enable from ams_admin where username = #{username}") AdminLoginInfoVO getLoginInfoByUsername(String username); }
2.4.2.5 编写mapper映射文件
- 举例
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); --> <select id="getLoginInfoByUsername" resultType="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO"> SELECT username, password, enable FROM ams_admin WHERE username=#{ username} </select>
2.4.2.6 使用BCryptPasswordEncoder()密码编码器
- 举例
/*** * 指定BCryptPasswordEncoder密码编码器 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
2.4.2.7 登录逻辑完整
- 举例
@Slf4j @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource(name = "adminLoginInfoMapper") private AdminLoginInfoMapper mapper; /*** * 指定密码不需要经过加密处理 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { AdminLoginInfoVO adminLoginInfoVO = mapper.getLoginInfoByUsername(s); log.debug("用户名:{}", adminLoginInfoVO.getUsername()); // 假设正确的用户名是root,匹配的密码是1234 if (adminLoginInfoVO==null) { log.debug("此用户名没有匹配的用户数据,将返回null"); return null; } log.debug("用户名匹配成功!准备返回此用户名匹配的UserDetails类型的对象"); UserDetails userDetails = User.builder() .username(adminLoginInfoVO.getUsername()) .password(adminLoginInfoVO.getPassword()) .disabled(false) // 账号状态是否禁用 .accountLocked(false) // 账号状态是否锁定 .accountExpired(false) // 账号状态是否过期 .credentialsExpired(false) // 账号的凭证是否过期 .authorities("这是一个临时使用的山寨的权限!!!") // 权限 .build(); log.debug("即将向Spring Security返回UserDetails类型的对象:{}", userDetails); return userDetails; } }
2.5 配置post可用
Spring Security框架设计了“防止伪造的跨域攻击”的防御机制,所以,默认情况下,自定义的POST请求是不可用的,简单的解决方案就是在Spring Security的配置类中禁用这个防御机制即可,例:
- 举例
@Slf4j @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { //关闭防止伪造的跨域攻击 http.cors().disable(); } }
2.6 使用前后端分离的登录
2.6.1 关闭自带的登录页面
- 配置类不再调用
http.formLogin()
2.6.2 使用控制器接收客户端登录请求
- 举例
@PostMapping("/login") public AdminLoginInfoVO login(AdminLoginInfoVO adminLoginInfoVO) { return adminLoginInfoVO; }
2.6.3 给登录控制器添加白名单
- 举例
String[] urls = { "/doc.html", "/**/*.css", "/**/*.js", "/swagger-resources", "/v2/api-docs", "/login" //添加白名单 }; // 配置授权访问 // 注意:以下授权访问的配置,是遵循“第一匹配原则”的,即“以最先匹配到的规则为准” // 例如:anyRequest()是匹配任何请求,通常,应该配置在最后,表示“除了以上配置过的以外的所有请求” // 所以,在开发实践中,应该将更具体的请求配置在靠前的位置,将更笼统的请求配置在靠后的位置 http.authorizeRequests() // 开始对请求进行授权 .mvcMatchers(urls) // 匹配某些请求 .permitAll() // 许可,即不需要通过认证就可以访问 .anyRequest