SpringSecurity

SpringSecuriry

SpringSecuriry简介:基于Spring提供声明式的安全访问可控制解决方案的安全框架(认证+授权),充分利用了IOC、DI、AOP功能。

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

自定义登录逻辑

SpringSecurity规定使用时容器必须有PasswordEncode的实例,故需要用配置类注入

PasswordEncoder接口:实现对密码进行加密和加密后的校验,这边推荐使用他的BCryptPasswordEncoder实现类。

在Spring Security中,用户输入的密码和查询到的密码(通常为哈希值)的一致性判断是通过PasswordEncoder实现的,具体过程如下:

 1. 用户认证请求:当用户尝试登录时,其提供的用户名和未加密的明文密码会被封装进一个UsernamePasswordAuthenticationToken对象中。

2. 认证管理器调用:接下来,这个AuthenticationToken会被传递给AuthenticationManager,这是Spring Security中处理认证请求的核心组件。

3. UserDetailsService调用AuthenticationManager根据UsernamePasswordAuthenticationToken中的用户名找到对应的UserDetailsService实现类,并调用loadUserByUsername方法来加载用户信息。这个方法返回一个包含用户信息的UserDetails对象,其中就包含了用户加密后的密码(哈希值)。

4. 密码校验:拥有用户信息后,AuthenticationManager会利用配置好的PasswordEncoder来校验用户输入的明文密码。它通过以下步骤完成:

@Configuration
public class SecurityConfig{
    @Bean
    public PasswordEncoder getPasswordEncode(){
        return new BCryptPasswordEncoder();
    }
}

自定义登录校验逻辑

@Service
public class UserSecurityServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder pe;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.查询数据库,获取用户名和密码
        User u=userMapper.findUser(username);
        //2。拿密码(密文)进行解析
        String password=pe.encode(u.getPassword);
        //返回的User和上面的User不是一个类,此处返回的User为SpringSecurity中的,AuthorityUtils.commaSeparatedStringToAuthorityList用于生成权限
        return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_A"));
        //admin和normal为权限,而ROLE_XXX为角色
    }
}

自定义登录页面

1.配置类补全

在 ‘自定义登录逻辑’ 配置的Config中继承WebSecurityConfigurerAdapter类并实现方法实现。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*
    WebSecurityConfigurerAdapter 是 Spring Security 早期版本中用于自定义安全配置的核心类。从 Spring Security 5.7 版本开始,此类已经被标记为过时,并在 Spring Security 6 中彻底移除。
    在新版 Spring Security 中,你可以直接实现 WebSecurityConfigurer 接口或使用 lambda 表达式来自定义安全配置:
     */
    @Autowired
    private UserSecurityServiceImpl userSecurityService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Bean
    public PasswordEncoder getPasswordEncode(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//表单提交
                //设置表单中登录的别名,默认为username和password
                .usernameParameter("uName")
                .passwordParameter("pWord")
                //当发现/submit认为是登录逻辑,此处必须与表单提交的地址一致,才会去执行UserSecurityServiceImpl
                .loginProcessingUrl("/submit")
                .loginPage("login.html")//自定义登录页
                //成功跳转必须是Post请求,所以需要在Controller中自定义跳转方法
                .successForwardUrl("/toMain")
                //这个是错误的跳转页面,Controller必须是Post
                .failureForwardUrl("/toError");

        http.authorizeRequests()
                //login.html为登录页,无需认证
                .antMatchers("/login.html").permitAll()
                //错误页面和登录页面都不需要被认证,下面为正则表达式,功能与上方一致
                //  .regexMatchers("/error.html").permitAll()  此方法效果一致
                .antMatchers("/error.html").permitAll()
                .anyRequest().authenticated();
                

        //csrf防护
        http.csrf().disable();
    }
    //将自定义的userDetail登录逻辑注册进配置类
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userSecurityService)
                .passwordEncoder(passwordEncoder);
    }
}

自定义登录处理器

面对前后端分离项目,如果使用上面的方法(successForwardUrl和failForwardUrl)无法跳转到在外的页面,需要定义一个处理器

1.自定义登录成功和失败处理器
public class PasswordSuccessHandle implements AuthenticationSuccessHandler {
    private String url;
    public PasswordSuccessHandle(String url){
        this.url=url;
    }
    //重定向至相应的页面
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //可添加其他逻辑
        response.sendRedirect(url);
    }
}
public class PasswordFailHandle implements AuthenticationFailureHandler {
    private String url;
    public PasswordFailHandle(String url){
        this.url=url;
    }
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //可添加其他逻辑
        response.sendRedirect(url);
    }
}
2.覆盖config中的相应配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
    //........

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//表单提交
                //............
                .loginPage("login.html")//自定义登录页

                //仅仅覆盖此处的配置,使用了自定义的处理器,参数为需要跳转的外部url
                .successHandler(new PasswordSuccessHandle("http://baidu.com"))
                .failureHandler(new PasswordFailHandle("/fail.html"));

        //......
    }
}

权限判断\IP判断

在上文登录校验中在登录成功后赋予了自定义的若干角色和权限,而下面对权限的配置是对其使用的方法之一

目前在MVC中,request.getRemoteAddr()获取访问的ip地址,同理Security也可以对ip地址进行权限的筛选。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //......

        http.authorizeRequests()
                //......

                //下面对角色权限的控制,只有指定的角色才能访问,角色取ROLE_XXX后面的XXX
                .antMatchers("main.html").hasAnyRole("A")
                //下面为权限级别的控制
                .antMatchers("manage.html").hasAnyAuthority("admin,normal")
                //只有以下ip才能访问下面的页面
                .antMatchers("main.html").hasIpAddress("198.159.3.1");
        //csrf防护
        http.csrf().disable();
    }
}

自定义响应状态处理方案

1.定义AccessDeniedHandle
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //设置需要处理的响应状态码
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //不然打印不出中文
        response.setHeader("Content-Type","application/json;charset=utf-8");
        PrintWriter writer=response.getWriter();
        writer.print("输出文字");
        writer.flush();
        writer.close();
    }
}
2.在WebSecurityConfig中引用
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
    @Autowired
    private MyAccessDeniedHandle myaccessDeniedHandle;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //......

        //设置自定义异常处理方案
        http.exceptionHandling().accessDeniedHandler(myaccessDeniedHandle);

       //......
    }

结合access自定义方法实现权限控制

1.创建对应权限控制方法,逻辑自定
@Service
public class PermissionHandle{
    //该方法的意思为loadByUserDetail返回的user中包含的权限必须包含可以访问的网址和角色
    //即 所包含的网址 允许 所包含的角色访问
    //return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_B,ROLE_A,main.html,manage.html"));

    public boolean hasPermission(HttpServletRequest request, Authentication authentication){
        Object obj = authentication.getPrincipal();
        if (obj instanceof UserDetails){
            UserDetails userDetails = (UserDetails) obj;
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
        }
        return false;
    }
}
2.在WebSecurityConfig中覆盖原有对于权限判断的代码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowire
    PermissionHandle permissionHandle;
    //......

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //......

        http.authorizeRequests()                   
        .anyRequest().access("@permissionHandle.hasPermission(request,authentication)");
        //......
    }

    //......
}

角色权限判断

1.@Secured注解

@Secured注解是springsecurity提供的,不用导入额外依赖,用于判断角色权限对于方法的访问

启动类添加@EnableGlobalMethodSecurity(securedEnabled = true),默认为false

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
//用于开启SpringSecurity提供的隐藏注解
public class OauthDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(OauthDemoApplication.class, args);
    }

}

在方法或类上添加@Secured注解,用于判断可以访问该方法的角色(角色在loadUserByUserName中返回时赋予),以 ’ROLE_‘ 开头。

@Controller
public class controller {
    //......
    @GetMapping("/v1/id")
    @Secured("ROLE_ABC")
    public String findUserId(){
        String id ="xxxxxx";
        return id;
    }
}
2.@PreAuthorize/@PostAuthorize注解

与上面的@Secured注解功能类似,但是大多情况下都是使用这两个注解,@PreAuthorize用于执行前的判断权限,而@PostAuthorize用于执行后的判断权限

启动类添加@EnableGlobalMethodSecurity(prePostEnabled = true),默认为false

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
//用于开启SpringSecurity提供的隐藏注解
public class OauthDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(OauthDemoApplication.class, args);
    }
}

在方法或类上添加@PreAuthorize/PostAuthorize注解,功能与@Secured同理,参数为hasRole('XXX')

@Controller
public class controller {
    //......
    @GetMapping("/v1/id")
    //@Secured("ROLE_ABC")'
    //与配置类中的hasROle不同的是:允许定于角色可以不以ROLE_开头
    @PreAuthorize("hasRole('ABC')")
    public String findUserId(){
        String id ="xxxxxx";
        return id;
    }
}

RememberMe功能

SpringSecurity中 RememberMe 为”记住我”功能,用户只需要在登录时添加 remember-me复框,取值为true。SpringSecurity 会自动把用户信息存储到数据源中,以后就可以不登录进行访问

1.由于需要将用户信息存储数据库中,故需要引入持久层依赖
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
2.在application.yml中设置对应持久层配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security
    username: root
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver
3.在webSecurityConfig中注册对应的Bean(PersistentTokenRepository )和 在configure中配置rememberMe的对应设置,并引入PersistentTokenRepository 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //......
    @Autowired
    private DataSource dataSource;

    @Autowired PersistentTokenRepository persistentTokenRepository;
    //(1) 定义PersistentTokenRepository 
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //自动建表,只有第一次启动才需要,第二次需要注释掉
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //......
        //(2)在configure中配置rememberMe
        http.rememberMe()
                //失效时间,单位为秒
                .tokenValiditySeconds(60*15)
                //自定义登录逻辑
                .userDetailsService(userSecurityService)
                //持久层对象
                .tokenRepository(persistentTokenRepository);


        //csrf防护
        http.csrf().disable();
    }

    //......
}
4.前端添加组件

退出登录

1.在webSecurityConfigurer中配置参数
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
    //......
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //......
        http.logout()
                //自定义登出请求
                .logoutUrl("/user/logout")
                //登出按钮跳转页面
                .logoutSuccessUrl("/login.html");

        //csrf防护
        http.csrf().disable();
    }

    //......
}
2.前端配置href

csrf防护

      上文中的http.csrf().disable();如果没有这个则会导致用户无法被认证。这行代码的含义是:关闭csrf防护。 CSRF为跨域请求伪造,通过伪造用户请求访问受信任站点的非法请求访问。

       跨域:只要网络协议、ip地址、端口中任意一项不相同则为跨域请求。

       从SringSecurity4.0开始,CSRF防护为默认开启,在部分开发过程中则会要求关闭默认防护,访问时携带参数名为_csrf,值为token(服务器产生)的内容,如果token和服务器记录的token一致则匹配成功,则访问成功。

2024.03.18

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值