Spring Security

Spring Security

1.1. 关于Spring Security

Spring Security主要解决了认证和授权相关的问题。

认证:验证用户提交的登录信息,判断是否可以通过。

授权:当认证通过后,给予通过认证的用户一些信息,后续,将根据这些信息来判断此用户是否允许执行某些访问。

1.2. 添加依赖

在Spring Boot项目,当需要使用Spring Security时,需要添加spring-boot-starter-security依赖:

<!-- Spring Boot Security,用于处理认证与授权 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

1.3. Spring Security的基本特点

当添加以上依赖项后,再次启动项目,Spring Security会执行自动配置,使得:

  • 此主机上所有的请求都必须先登录才可以访问,并提供了登录页面,包括根本不存在的URL

    • 此特点是由WebSecurityConfigurerAdapter类中的protected void configure(HttpSecurity http)方法决定的
      • 此配置方法中的http.formLogin()将决定是否开启登录表单
  • 默认的用户名是user,默认是密码在启动日志中,每次启动都会变化

    • Using generated security password: 92062850-fb3d-4b86-be10-209ac26c143e
      
  • 当登录成功后,会自动跳转到此前尝试访问的URL

    • 例如,当尝试访问 http://localhost:9081/ 时,由于没有登录,会自动重定向到 http://localhost:9081/login 显示了登录页面,当登录成功后,会自动重定向到此前尝试访问的 http://localhost:9081/
  • Spring Security默认使用Session保存用户的登录信息

    • 例如,当关闭浏览器后,再次访问,需要重新登录
  • 通过 http://localhost:9081/logout 退出登录

  • 自带BCryptPasswordEncoder,可以用于使用BCrypt算法对密码进行加密处理

  • 如果项目运行时Spring容器中有密码编码器(PasswordEncoder),Spring Security框架会自动使用它

  • Spring Security默认开启了“禁止跨域的异步提交”,避免“伪造的跨域攻击”

1.4. 关于BCrypt算法

BCrypt算法是一种基于哈希算法的算法,所以,这种算法也是不可逆的!

通过BCrypt算法进行编码后的结果,长度固定为60字符。

使用同一个原文进行反复编码,每次得到的结果都是不同的,因为在编码过程中,BCrypt使用了随机的盐,并且,使用的盐也作为编码结果的一部分保存了下来。

在开发实现中,通常,可以使用配置类中的@Bean方法来创建BCryptPasswordEncoder对象,此对象将是由Spring进行管理的,当需要使用时,自动装配即可,例如:

@Slf4j
@Configuration
public class SecurityConfiguration {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
@Slf4j
@Service
public class AdminServiceImpl implements IAdminService {

    @Autowired
    private PasswordEncoder passwordEncoder;
    
}

1.5. 关于401403这2个HTTP响应码

在HTTP协议中,401表示未认证的,通常是没有成功登录的,403表示未授权的,通常是已经登录,但是不具备相关的操作权限。

在Spring Security中,在许多“禁止”的场景中都会使用403,并不完全符全HTTP协议的规范。

1.6. 关于伪造跨域攻击

在多选项卡的浏览器中,如果在第1个选项卡中登录了某个平台,在同一个窗口中打开其它选项卡,访问同一平台,都会被平台识别,视为“已通过认证”。

所谓的伪造跨域攻击,就是利用以上特性,在网页源代码中隐藏一些恶意访问的、会自动提交的请求URL(例如使用隐藏的<img>标签的src属性),例如,当用户在第1个选项卡登录了某银行系统,打开的第2个选项卡是有恶意代码的平台,第2个选项卡的网页发出“转账”的请求,会被银行系统视为“已通过认证的”。

  • 提示:以上只是举例,事实上,现在防止这种做法的技术已经非常成熟,不会出现此问题,并且,银行转账通常都需要再次输出密码,而不会收到请求就直接转账

在使用Spring Security时,应该自定义配置类,继承自WebSecurityConfigurerAdpater,并重写void configure(HttpSecurity http)方法,在其中调用http.csrf()方法。

1.7. 关于伪造跨域攻击

Spring Security在防止伪造跨域攻击时,会自动生成值为UUID的Token(票据 / 令牌),并且,将此值响应给客户端,针对客户端后续提交的 POST / DELETE / PUT / PATCH 类型的请求,都要求携带名为 _csrf 的参数,且值就是此UUID,如果客户端提交请求时没有携带此值,则视为“伪造的跨域攻击”,将响应 403 错误。

在继承了WebSecurityConfigurerAdapter的配置类中,重写configurer(HttpSecurity http)方法,调用http.csrf().disable()即可禁用它,即不再检查各请求是否为“伪造跨域”的访问。

提示:禁用后,会存在被伪造跨域攻击的风险,但是,我们会在后续的学习中解决它。

1.8. 使用数据库中的账号实现登录认证

要能够使用数据库中的账号实现登录认证,必须至少实现”根据用户名查询用户登录信息“的查询功能!

则在Mapper层,需要:

  • 在项目的根包下创建pojo.vo.AdminLoginInfoVO.java类,在此类中添加登录时所需的数据属性:

    • @Data
      public class AdminLoginInfoVO implements Serializable {
          // 必须包括:id, username, password, enable
      }
      
  • AdminMapper.java接口中添加查询方法:

    • AdminLoginInfoVO getLoginInfoByUsername(String usernanme);
      
  • AdminMapper.xml中配置以上抽象方法映射的SQL语句:

    • <!-- AdminLoginInfoVO getLoginInfoByUsername(String usernanme); -->
      <select id="getLoginInfoByUsername" resultMap="LoginResultMap">
          SELECT
              <include refid="LoginQueryFields"/>
          FROM
              ams_admin
          WHERE
              username=#{username}
      </select>
      
      <sql id="LoginQueryFields">
          <if test="true">
              id, username, password, enable
          </if>
      </sql>
      
      <resultMap id="LoginResultMap" 
                 type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
          <id column="id" property="id"/>
          <result column="username" property="username"/>
          <result column="password" property="password"/>
          <result column="enable" property="enable"/>
      </resultMap>
      
  • AdminMapperTests.java中编写并执行测试:

    • @Test
      void testGetLoginInfoByUsername() {
          String username = "root";
          AdminLoginInfoVO loginInfoByUsername = mapper.getLoginInfoByUsername(username);
          log.debug("根据用户名【{}】查询登录信息:{}", username, loginInfoByUsername);
      }
      

Spring Security在执行认证时,会根据用户提交的用户名,自动调用UserDetailsService接口类型的对象中的UserDetails loadUserByUsername(String username);方法,当得到返回的UserDetails后,会自动处理后续的细节,例如验证密码是否正确、将认证信息(登录成功后的用户信息)保存下来,便于后续识别用户身份等。

所以,在根包下创建security.UserDetailsServiceImpl类,实现UserDetailsService接口,并且,在类上添加@Service注解,并实现接口中声明的抽象方法:

package cn.tedu.csmall.passport.security;

import cn.tedu.csmall.passport.mapper.AdminMapper;
import cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private AdminMapper adminMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("Spring Security自动调用loadUserByUsername()方法获取用户名为【{}】的用户详情……", s);

        AdminLoginInfoVO loginInfoByUsername = adminMapper.getLoginInfoByUsername(s);
        log.debug("从数据库中查询到的用户信息:{}", loginInfoByUsername);
        if (loginInfoByUsername == null) {
            String message = "登录失败,用户名不存在!";
            log.warn(message);
            throw new BadCredentialsException(message);
        }

        UserDetails userDetails = User.builder()
                .username(loginInfoByUsername.getUsername())
                .password(loginInfoByUsername.getPassword())
                .accountExpired(false) // 账号是否已过期
                .accountLocked(false) // 账号是否已锁定
                .credentialsExpired(false) // 凭证是否已过期
                .disabled(loginInfoByUsername.getEnable() == 0) // 账号是否已禁用
                .authorities("临时设置的权限,避免报错,暂无意义") // 权限,【注意】必须调用此方法表示此用户具有哪些权限
                .build();
        log.debug("即将向Spring Security框架返回UserDetails对象:{}", userDetails);
        return userDetails;
    }

}

提示:一旦Spring窗口存在UserDetailsService接口类型的对象,在启动项目时(包括执行测试时),将不再生成随机的临时密码,此前使用的user账号也将不再允许使用!

完成以上代码后,可以在Security的配置类中,通过http.formLogin();方法启用登录页面,并启动项目,通过 http://localhost:9081/login 打开登录页面,此时,可以使用数据库的账号尝试登录。

注意:此前完成的查询功能中,必须查询passwordenable这2个字段的值!

注意:因为Spring Security会自动应用密码编码器(在Security配置类中使用@Bean方法配置的PasswordEncoder),数据库中的密码值必须是BCrypt编码结果!

注意:必须确保尝试登录的账号的enable值是有效的,如果为null,则会导致NPE!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值