> 文档中心 > Spring Security登录的认证和授权

Spring Security登录的认证和授权


一、Spring Security简介

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

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

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


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

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

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

    org.springframework.bootspring-boot-starter-security

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

【User实体类实现UserDetails接口

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

// true: 账号未过期.@Overridepublic boolean isAccountNonExpired() {    return true;}
// true: 账号未锁定.@Overridepublic boolean isAccountNonLocked() {    return true;}
// true: 凭证未过期.@Overridepublic boolean isCredentialsNonExpired() {    return true;}
// true: 账号可用.@Overridepublic boolean isEnabled() {    return true;}
// 获取用户权限@Overridepublic Collection getAuthorities() {    List 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方法:

@Servicepublic 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: 用于封装认证信息(账号密码等)的接口, 不同的实现类代表不同类型的认证信息.
@Configurationpublic 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);    }}

认证成功后,结果会通过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进行授权。

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


三、thymeleaf + spring security

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

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

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

!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->org.thymeleaf.extrasthymeleaf-extras-springsecurity5

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