java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot Security入门

SpringBoot Security从入门到实战示例教程

作者:lakernote

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,接下来通过本文给大家介绍SpringBoot Security从入门到实战示例教程,感兴趣的朋友一起看看吧

前言

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(默认)

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 强哈希函数,开发者在使用时可以选择提供 strengthSecureRandom 实例。strength 取值在 4~31 之间(默认为 10)。strength 越大,密钥的迭代次数越多(密钥迭代次数为 2^strength)。如果是数据库认证,库里的密码同样也存放加密后的密码。同一密码每次 Bcrypt 生成的结果都会变化不会重复。

配置AuthenticationManagerBuilder 认证用户、角色权限

支持直接配置内存认证模式和配置UserDetailsServiceBean方式

内存认证模式,实际项目不用这个哦。(仅做了解)

 /**
     * 配置用户及其对应的角色
     */
    @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中,即存储在内存中。

自定义UserDetailsServiceBean方式,实际项目都是使用这个,可定制化程度高。

步骤一:定义一个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 个方法:

返回的密码和用户输入的登录密码不匹配,会自动抛出 BadCredentialsException 异常。

@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访问权限

  /**
     * 配置 URL 访问权限
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //
        http    // 1.开启 HttpSecurity 配置
                .authorizeRequests()
                // laker/** 模式URL必须具备laker.query
                .antMatchers("/laker/**").hasAnyAuthority("laker.query")
                // 用户访问其它URL都必须认证后访问(登录后访问)
                .anyRequest().authenticated()
                .and()
                // 2.开启表单登录,前后端分离的时候不用这个
                .formLogin()
                // 未登录时 重定向的url 默认是/login 内置的页面,可以自己自定义哈。一般前后端分离,不用这个
//                .loginPage("/login")
                //
//                .defaultSuccessUrl("/user",true)
//                .usernameParameter("username") // default is username
//                .passwordParameter("password") // default is password
//                .loginPage("/authentication/login") // default is /login with an HTTP get
//                .failureUrl("/authentication/login?failed") // default is /login?error
//                .loginProcessingUrl("/authentication/login/process") // default is /login
                .and()
                // 3.关闭csrf,前后端分离不需要这个。
                .csrf().disable();
                //授权码模式需要 会弹出默认自带的登录框
                http.httpBasic();   
        		// 开启注销登录的配置 
                http.logout()
                    // 配置注销登录请求URL为"/logout"(默认也就是 /logout)
                    .logoutSuccessUrl("/logout")
                    .clearAuthentication(true) // 清除身份认证信息
                    .invalidateHttpSession(true) // 使 session 失效;
    }

    anyRequest          |   匹配所有请求路径
    access              |   SpringEl表达式结果为true时可以访问
    anonymous           |   匿名可以访问 所有人都能访问,但是带上 token访问后会报错403
    denyAll             |   用户不能访问 所有人都能访问,包括带上 token 访问
    fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
    hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
    hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
    hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
    hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
    hasRole             |   如果有参数,参数表示角色,则其角色可以访问
    permitAll           |   用户可以任意访问
    rememberMe          |   允许通过remember-me登录的用户访问
    authenticated       |   用户登录后可访问

自定义successHandler

登录成功后默认是重定向url,我们可以自定义返回json用于前后端分离场景以及其他逻辑,例如成功之后发短信等。

http.formLogin().successHandler((req, resp, authentication) -> {
    // 发短信哈
    Object principal = authentication.getPrincipal();
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write(new ObjectMapper().writeValueAsString(principal));
    out.flush();
    out.close();
})

自定义failureHandler

登录失败回调

http.formLogin().failureHandler((req, resp, e) -> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write(e.getMessage());
    out.flush();
    out.close();
})

自定义未认证处理

http.exceptionHandling()
.authenticationEntryPoint((req, resp, authException) -> {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            out.write("尚未登录,请先登录");
            out.flush();
            out.close();
        }
);

自定义权限不足处理

http.exceptionHandling()
				//没有权限,返回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(objectMapper.writeValueAsString(map));
					out.flush();
					out.close();
				})

自定义注销登录

.logout()
.logoutUrl("/logout")
.logoutSuccessHandler((req, resp, authentication) -> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("注销成功");
    out.flush();
    out.close();
})

前后端分离场景

上面的都是入门的,实际项目中一般都是前后端分离的,在登录时都是自定义登录接口,例如登录接口是restful风格,增加了其他的验证码参数,还使用jwt来完成登录鉴权等。

提供登录接口

该接口需要在配置当中放行,未授权访问需要授权的请求时,会返回401或者403状态码,前端可以根据这个进行路由提示处理。

@RestController
public class LoginController {
   @Autowired
   LoginService ...
   @PostMapping("/login")
   public  login(@RequestBody Login login){
       ...
       return token;
   }
}

Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象.

@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    public  doLogin(Login login) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password;
        Authentication  authenticate
        try {         // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
             authenticate = authenticationManager.authenticate(authenticationToken);
        } catch (AuthenticationException e) {
            e.printStackTrace();
        }
 
        if (Objects.isNull(authenticate)) {
            //用户名密码错误
            throw new ServicesException(...);
        }
        User authUser = (User) authenticate.getPrincipal();
        String token = JwtUtil.createJWT(username);
        Map<String, String> map = new HashMap<>();
        map.put("token", token);
        return map;
    }
}

自定义认证过滤器

坊间有2种实现方式。

方式一:继承UsernamePasswordAuthenticationFilter的写法需要使用登陆成功处理器、失败处理器等,还是需要按照security这一套来玩。

Spring Security默认支持表单请求登录的源码UsernamePasswordAuthenticationFilter.java

方式二:使用Filter的写法没有任何限制怎么玩都行,比如说添加其他参数验证码,返回json,token鉴权等。

@Component
public class LakerOncePerRequestFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (!StringUtils.isEmpty(token) )
        {
            // 校验token ...
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities;
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                                     SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}                                                                                                           //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(lakerOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);

鉴权

1.注解鉴权

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
...
@RestController
public class Controller {
    @GetMapping("/hello")
    @PreAuthorize("hasAnyAuthority('laker.query')")
    public String test() {
}

2.自定义Bean动态鉴权

因为@PreAuthorize支持SpringEL表达式,所以可以支持自定义SpringBean动态鉴权。

@Component("rbacService")
public class LakerRBACService {
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            UserDetails userDetails=(UserDetails)principal;

            /**
             * 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true
             */
            //本次要访问的资源
              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;
            /**
             * 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true
             */
            //本次要访问的资源
            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);
    }
    /**
     * 自定义表达式
     * @param username 具有权限的用户账号
     */
    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("/**",cores.applyPermitDefaultValues());
        return source;
    }
}

全局配置

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;

    @Autowired
    TokenFilter tokenFilter;

    /**
     * 配置 URL 访问权限
     */
    @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);
    }
    /**
     * 适用于静态资源的防拦截,css、js、image 等文件
     * 配置的url不会保护它们免受CSRF、XSS、Clickjacking等的影响。
     * 相反,如果您想保护端点免受常见漏洞的侵害,请参阅configure(HttpSecurity)
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**");
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     */
    @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入门内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文