Spring Security登录的认证和授权

2022-09-03 07:59:50

目录

一、Spring Security简介

二、入门实现登录的认证和授权

三、处理 CSRF 攻击

四、thymeleaf + spring security


一、Spring Security简介

        Spring Security是一个专注于为Java应用程序提供身份认证授权的框架,是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

spring security 的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)


二、入门实现登录的认证和授权

具体代码可在码云拷贝:项目源码

【在pom.xml文件导入依赖】

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

导入security依赖后,会立刻对项目产生影响,再次访问项目的任何页面都需要登录,登录账号默认为:user,在日志中给出随机密码

【User实体类实现UserDetails接口】

在用户的实体类User中继承UserDetails接口,并实现下面五个方法:

// true: 账号未过期.
@Override
public boolean isAccountNonExpired() {
    return true;
}
// true: 账号未锁定.
@Override
public boolean isAccountNonLocked() {
    return true;
}
// true: 凭证未过期.
@Override
public boolean isCredentialsNonExpired() {
    return true;
}
// true: 账号可用.
@Override
public boolean isEnabled() {
    return true;
}
// 获取用户权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> list = new ArrayList<>();
    list.add(new GrantedAuthority() {
        @Override
        public String getAuthority() {
            // type表示自定义用户的身份, 1是管理员, 其余是普通用户
            switch (type) { 
                case 1:
                    return "ADMIN";
                default:
                    return "USER";
            }
        }
    });
    return list;
}

【在UserService中实现UserDetailsService方法】

在用户的业务逻辑中实现UserDetailsService方法,并实现loadUserByUsername方法:

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    public User findUserByName(String username) {
        return userMapper.selectByName(username);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return this.findUserByName(username);
    }
}

【创建SecurityConfig配置类, 实现过滤】

配置类继承WebSecurityConfigurerAdapter类,并重写 configuer 方法,需要重写三种重载形式的configure

  • AuthenticationManager: 认证的核心接口.
  • AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
  • ProviderManager: AuthenticationManager接口的默认实现类.
  • AuthenticationProvider: ProviderManager将持有一组AuthenticationProvider, 每个AuthenticationProvider负责一种认证.
  • 委托模式: ProviderManager将认证委托给AuthenticationProvider
  • Authentication: 用于封装认证信息(账号密码等)的接口, 不同的实现类代表不同类型的认证信息.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略静态资源的访问
        web.ignoring().antMatchers("/resource/**");
    }

    // AuthenticationManager: 认证的核心接口.
    // AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
    // ProviderManager: AuthenticationManager接口的默认实现类.
    // 认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 1. 内置的认证规则
        // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));

        // 2. 自定义认证规则
        // AuthenticationProvider: ProviderManager将持有一组AuthenticationProvider, 每个AuthenticationProvider负责一种认证.
        // 委托模式: ProviderManager将认证委托给AuthenticationProvider.
        auth.authenticationProvider(new AuthenticationProvider() {
            // Authentication: 用于封装认证信息(账号密码等)的接口, 不同的实现类代表不同类型的认证信息.
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String username = authentication.getName();
                String password = (String) authentication.getCredentials();

                User user = userService.findUserByName(username);
                if(user == null) {
                    throw new UsernameNotFoundException("账号不存在!");
                }

                password = CommunityUtil.md5(password + user.getSalt());
                if(!password.equals(user.getPassword())) {
                    throw new BadCredentialsException("密码不正确!");
                }

                // principal: 认证的主要信息; credentials: 证书; authorities: 权限
                return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
            }

            // 当前的AuthenticationProvider支持哪种类型的认证.
            @Override
            public boolean supports(Class<?> aClass) {
                // UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类(账号密码).
                return UsernamePasswordAuthenticationToken.class.equals(aClass);
            }
        });
    }

    // 授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登录相关配置
        http.formLogin()
                .loginPage("/loginpage")// 登录时的页面
                .loginProcessingUrl("/login")// 发送登录请求的路径
                .successHandler(new AuthenticationSuccessHandler() {
                    // 登录成功的处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect(request.getContextPath() + "/index");
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    // 登陆失败的操作
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                        request.setAttribute("error", e.getMessage());
                        request.getRequestDispatcher("/loginpage").forward(request, response);
                    }
                });

        // 退出相关配置
        http.logout()
                .logoutUrl("/logout")// 退出登录的路径
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    // 退出成功的处理
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect(request.getContextPath() + "/index");
                    }
                });

        // 授权配置
        http.authorizeRequests()
                .antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")// 为指定页面配置指定权限
                .antMatchers("/admin").hasAnyAuthority("ADMIN")
                .and().exceptionHandling().accessDeniedPage("/denied");// 访问失败后要去的路径

        // 增加Filter, 处理验证码
        http.addFilterBefore(new Filter() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) servletRequest;
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                if(request.getServletPath().equals("/login")) {
                    // 如果是登录页面, 才处理验证码
                    String verifyCode = request.getParameter("verifyCode");
                    if(verifyCode == null || !verifyCode.equals("12345")) {
                        // 验证码不正确
                        request.setAttribute("error", "验证码错误!");
                        request.getRequestDispatcher("/loginpage").forward(request, response);
                        return;
                    }
                }
                // 让请求继续向下执行
                filterChain.doFilter(request, response);
            }
        }, UsernamePasswordAuthenticationFilter.class);

        // 记住我
        http.rememberMe()
                .tokenRepository(new InMemoryTokenRepositoryImpl())
                .tokenValiditySeconds(3600 * 24)
                .userDetailsService(userService);
        
/*
        // 权限不够时的处理
        http.exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    // 没有登录时的处理
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                        // 判断请求是异步请求还是同步请求
                        String xRequestedWith = request.getHeader("x-requested-with");
                        if("XMLHttpRequest".equals(xRequestedWith)) {
                            response.setContentType("application/plain;charset=utf-8");
                            PrintWriter writer = response.getWriter();
                            writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
                        } else {
                            response.sendRedirect(request.getContextPath() + "/login");
                        }
                    }
                })
                .accessDeniedHandler(new AccessDeniedHandler() {
                    // 没有权限时的处理
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
                        // 判断请求是异步请求还是同步请求
                        String xRequestedWith = request.getHeader("x-requested-with");
                        if("XMLHttpRequest".equals(xRequestedWith)) {
                            response.setContentType("application/plain;charset=utf-8");
                            PrintWriter writer = response.getWriter();
                            writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限"));
                        } else {
                            response.sendRedirect(request.getContextPath() + "/denied");
                        }
                    }
                });

        // Security底层默认会拦截/logout路径请求, 进行退出的处理
        // 覆盖它默认的逻辑, 才能执行我们自己退出的代码
        http.logout().logoutUrl("/securitylogout");
*/
    }
}

认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.

@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
    // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
    Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if(obj instanceof User) {
        model.addAttribute("loginUser", obj);
    }
    return "/index";
}

三、处理 CSRF 攻击

csrf攻击:当用户提交表单数据时,可能会收到csrf的攻击,获取表单中的登录信息,从而盗取用户信息。

Spring Security默认对普通请求的表单的隐藏域中加上csrf的检验序列(令牌),而csrf病毒攻击时不持有令牌将会被拒绝访问
但对于AJAX请求,Spring Security没有默认的检验序列,所以需要我们自动配置。

 对AJAX请求配置检验序列:

在index页面手动配置生成CSRF令牌:

在js文件的发布内容的事件中将CSRF令牌设置到请求的消息头中:

授权时加上http.csrf().disable();可以废弃Spring Security对csrf攻击的保护。

注:

在 SecurityConfig 类的配置中,若没有使用 Filter 过滤器认证用户信息,只是授权用户无法实现过滤功能,需要在 Interceptor 中拦截用户并构建用户的认证信息,然后手动地存入 SecurityContext 中,以便 Security 进行授权,而在 SecurityConfig 类就不用再进行认证了,只需授权就可以了

在请求结束时记得清理用户的认证信息


四、thymeleaf + spring security

权限管理可以分为两个层面:

  1. 在服务器层面过滤掉没有相应权限的用户,不让其访问对应的功能
  2. 在前端页面中友好的向不同身份权限的用户展示不同的功能

这里,thymeleaf 内置的标签支持 spring security,但若想使用其功能还需要导入相关包,选择对应父pom中的版本。

!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

在页面顶端声明命名空间,在github文档末尾可找到:

  • 作者:李巴巴
  • 原文链接:https://blog.csdn.net/weixin_52850476/article/details/123056926
    更新时间:2022-09-03 07:59:50