前言
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。它的核心是一组过滤器链,不同的功能经由不同的过滤器。这篇文章给大家讲解SpringBoot Security从入门到实战,内容如下所示:
入门
测试接口
假设我们用下面的接口做权限测试。
@RestController
public class LakerController {
@GetMapping("/laker")
public String laker() {
return IdUtil.simpleUUID();
}
@GetMapping("/laker/q")
public String lakerQ() {
return IdUtil.simpleUUID();
}
}
浏览器访问:http://localhost:8080/laker
,结果如下:
增加依赖
在 pom.xml,添加 spring-boot-starter-securtiy 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
再次访问http://localhost:8080/laker
,结果如下:
简要解析
我们访问http://localhost:8080/laker
,security判断我们没有登录,则会302重定向到http://localhost:8080/login
(默认)
- security会返回一个默认的登录页。
- 默认用户名为:
user
,密码在服务启动时,会随机生成一个,可以查看启动日志如下:
2022-05-02 21:01:03.697 INFO 17896 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2022-05-02 21:01:03.825 INFO 17896 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :Using generated security password: e53fef6a-3f61-43c3-9609-ce88fd7c0841
当然,可以通过配置文件设置默认的用户名、密码、角色。
spring:
security:
user:
# 默认是 user
name: laker
password: laker
roles:
- ADMIN
- TESTER
以上的默认都是可以修改的哈。
判断是否登录使用传统的cookie session模式哈,JSESSIONID E3512CD1A81DB7F2144C577BA38D2D92 HttpOnly true
自定义配置
实际项目中我们的用户、密码、角色、权限、资源都是存储在数据库中的,我们可以通过自定义类继承 WebSecurityConfigurerAdapter,从而实现对 Spring Security 更多的自定义配置。
@Configuration
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
...
}
配置密码加密方式
必须配置,否则报空指针异常。
@Bean
PasswordEncoder passwordEncoder() {
// 不加密
return NoOpPasswordEncoder.getInstance();
}
Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder:
BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 取值在 4~31 之间(默认为 10)。strength 越大,密钥的迭代次数越多(密钥迭代次数为 2^strength)。如果是数据库认证,库里的密码同样也存放加密后的密码。同一密码每次 Bcrypt 生成的结果都会变化不会重复。
配置AuthenticationManagerBuilder 认证用户、角色权限
支持直接配置内存认证模式和配置UserDetailsService
Bean方式
内存认证模式,实际项目不用这个哦。(仅做了解)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("123").roles("ADMIN", "USER")
.and()
.withUser("laker").password("123").roles("USER");
}
上面在UserDetailsService的实现之一InMemoryUserDetailsManager中,即存储在内存中。
自定义UserDetailsService
Bean方式,实际项目都是使用这个,可定制化程度高。
步骤一:定义一个LakerUserService
实现UserDetailsService
,配置成SpringBean。该方法将在用户登录时自动调用。
@Service
public class LakerUserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// username 就是前端传递的例如 laker 123,即 laker
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("账户不存在!");
}
user.setAuthorities(...);
return user;
}
}
username
password(加密后的密码)
authorities
返回的Bean中以上3个都不能为空。返回User后由系统提供的 DaoAuthenticationProvider 类去比对密码是否正确。
Spring Security默认支持表单请求登录的源码,UsernamePasswordAuthenticationFilter.java
步骤二:在把自定义的LakerUserService
装载进去.
@Autowired
UserService userService;
...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
步骤三:其中我们的业务用户User必须要实现UserDetails
接口,并实现该接口中的 7 个方法:
- getAuthorities():获取当前用户对象所具有的权限信息
- getPassword():获取当前用户对象的密码
返回的密码和用户输入的登录密码不匹配,会自动抛出 BadCredentialsException 异常。
- getUsername():获取当前用户对象的用户名
- isAccountNonExpired():当前账户是否未过期
- isAccountNonLocked():当前账户是否未锁定
- 返回了 false,会自动抛出 AccountExpiredException 异常。
- isCredentialsNonExpired():当前账户密码是否未过期
- isEnabled():当前账户是否可用
@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<String> authorities;
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authoritiesList = new ArrayList<>();
for (String authority : authorities) {
authoritiesList.add(new SimpleGrantedAuthority(authority));
}
return authoritiesList;
}
}
配置HttpSecurity Url访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
//
http // 1.开启 HttpSecurity 配置
.authorizeRequests()
// laker
//本次要访问的资源
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getMethod() + "" + request.getRequestURI());
//用户拥有的权限中是否包含请求的url
return userDetails.getAuthorities().contains(simpleGrantedAuthority);
}
return false;
}
public boolean hasPermission() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
//本次要访问的资源
HttpServletRequest request =((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getRequestURI());
//用户拥有的权限中是否包含请求的url
return userDetails.getAuthorities().contains(simpleGrantedAuthority);
}
return false;
}
}
// controller方法
@PreAuthorize("@rbacService.hasPermission()")
public String test() {
}
// 或者高级的全局url鉴权
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
http.authorizeRequests() //设置授权请求,任何请求都要经过下面的权限表达式处理
.anyRequest().access("@rbacService.hasPermission(request,authentication)") //权限表达式
3.扩展默认方法自定义扩展根对象SecurityExpressionRoot
https://www.jb51.net/article/245172.htm
1.创建自定义根对象
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
public CustomMethodSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
public boolean hasUser(String... username) {
String name = this.getAuthentication().getName();
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String[] names = username;
for (String nameStr : names) {
if (name.equals(nameStr)) {
return true;
}
}
return false;
}
}
2.创建自定义处理器
创建自定义处理器,主要是重写创建根对象的方法。
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
root.setThis(invocation.getThis());
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(getDefaultRolePrefix());
return root;
}
}
3.配置GlobalMethodSecurityConfiguration
之前我们使用@EnableGlobalMethodSecurity开启全局方法安全,而这些全局方法级别的安全配置就在GlobalMethodSecurityConfiguration配置类中。
可以扩展这个类来自定义默认值,但必须确保在类上指定@EnableGlobalMethodSecurity 注解,否则会bean冲突报错。
@Configuration
// 将EnableGlobalMethodSecurity注解移到这里
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new CustomMethodSecurityExpressionHandler();
}
}
4.controller使用自定义方法
@PreAuthorize("hasUser('laker','admin')")
public String test() {
...
}
登出
http.logout().logoutUrl("/logout").logoutSuccessHandler((request, response, authentication) -> {
// 删除用户token
...
// 返回json
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
out.write("OK");
out.flush();
out.close();
});
跨域
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();//允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
http.csrf().disable();//关闭CSRF防御
}
@Configuration
public class CrosConfig {
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration cores=new CorsConfiguration();
cores.setAllowCredentials(true);//允许客户端携带认证信息
//springBoot 2.4.1版本之后,不可以用 * 号设置允许的Origin,如果不降低版本,则在跨域设置时使用setAllowedOriginPatterns方法
// cores.setAllowedOrigins(Collections.singletonList("*"));//允许所有域名可以跨域访问
cores.setAllowedOriginPatterns(Collections.singletonList("*"));
cores.setAllowedMethods(Arrays.asList("GET","POST","DELETE","PUT","UPDATE"));//允许哪些请求方式可以访问
cores.setAllowedHeaders(Collections.singletonList("*"));//允许服务端访问的客户端请求头
// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
cores.addExposedHeader(jsonWebTokenUtil.getHeader());
// 注册跨域配置
// 也可以使用CorsConfiguration 类的 applyPermitDefaultValues()方法使用默认配置
source.registerCorsConfiguration("
@Override
protected void configure(HttpSecurity http) throws Exception {
//
http // 1.过滤请求
.authorizeRequests()
// 2.对于登录login 验证码captcha 允许访问
.antMatchers("/login").permitAll()
// 用户访问其它URL都必须认证后访问(登录后访问)
.anyRequest().authenticated()
.and()
// 3.关闭csrf
.csrf().disable()
// 4.基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 5.页面能不能以 frame、 iframe、 object 形式嵌套在其他站点中,用来避免点击劫持(clickjacking)攻击
.and().headers().frameOptions().disable();
// 异常处理
http.exceptionHandling()
// 未认证返回401
.authenticationEntryPoint((req, response, authException) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
out.write("尚未登录,请先登录");
out.flush();
out.close();
})
// 没有权限,返回403 json
.accessDeniedHandler((request, response, ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", 403);
map.put("message", "权限不足");
out.write(JSONUtil.toJsonPrettyStr(map));
out.flush();
out.close();
});
// 配置登出
http.logout().logoutUrl("/logout").logoutSuccessHandler((request, response, authentication) -> {
// 删除用户token
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
out.write("OK");
out.flush();
out.close();
});
// 添加JWT filter
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
}
参考:
https://blog.csdn.net/X_lsod/article/details/122914659
https://blog.csdn.net/godleaf/article/details/108318403
https://blog.csdn.net/qq_43437874/article/details/119543579
到此这篇关于SpringBoot Security从入门到实战示例教程的文章就介绍到这了,更多相关SpringBoot Security入门内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!