IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring Boot+Security (Day12) -> 正文阅读

[Java知识库]Spring Boot+Security (Day12)

1. Spring Security(续)

1.1. 关于伪造跨域攻击(续前日5.6)

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

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

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

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

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

则在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!

1.3. 现有的问题

目前,已经可以使用数据库中的账号进行登录认证,但是,存在以下问题:

  • 当前做法并不是前后端分离的
  • Spring Security默认使用Session保存认证信息

1.4. 关于Session

HTTP协议本身是无状态协议!

  • 无状态:同一个客户端的多次请求,服务器并不能识别此客户端的身份,例如:第2次收到此客户端的请求时,并不知道此客户端此前已经提交过一次请求,更不知道第1次处理此客户端请求时产生的数据

在开发实践中,是需要明确客户端身份的,所以,从技术层面,使用了Session来解决HTTP协议无状态的问题。

Session的本质是一个MAP结构的数据,当客户端首次向服务器端提交请求时,服务器端会响应一个Session ID到客户端,客户端在后续的访问中,都会在请求中自动携带此Session ID,同时,服务器端的内存中会存在每个Session ID对应的Session数据,从而,每个客户端都可以访问到自己的此前存入的数据。

由于Session是在服务器端的内存中的数据,因此,默认情况下,并不适合于集群系统,更不适用于分布式系统。

1.5. 使用前后端分离的方式处理认证

在自定义的SecurityConfiguration配置类中添加:

@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
    // 调用父类的方法得到AuthenticationManager
    return super.authenticationManager();
}

因为自定义的配置类继承自WebSecurityConfigurerAdapter类,此父类中存在authenticationManager()方法,可以返回AuthenticationManager对象,可用于后续自行调用authenticate(),使得Spring Security执行认证!所以,为了保证后续代码可以调用AuthenticationManagerauthenticate()方法,应该在当前配置类中重写authenticationManager()方法(如以上代码所示),其主要目的是为了调用父类的方法,并且,在重写的方法上添加@Bean注解,由于当前类也是配置类(有@Configuration注解),则Spring会自动调用此@Bean注解的方法,得到AuthenticationManager对象并保存在Spring容器中,以至于后续编写代码时,可以随时自动装配AuthenticationManager对象!

在根包下创建pojo.dto.AdminLoginInfoDTO类,在类中声明usernamepassword这2个属性:

package cn.tedu.csmall.passport.pojo.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class AdminLoginInfoDTO implements Serializable {

    private String username;
    private String password;

}

IAdminService中自定义登录认证的方法:

void login(AdminLoginInfoDTO adminLoginInfoDTO);

AdminServiceImpl中实现以上新增的抽象方法:

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void login(AdminLoginInfoDTO adminLoginInfoDTO) {
    // 调用AuthenticationManager的authenticate()方法执行认证
}

具体实现为:

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void login(AdminLoginInfoDTO adminLoginInfoDTO) {
    log.debug("开始处理【登录认证】的业务,参数:{}", adminLoginInfoDTO);

    // 调用AuthenticationManager的authenticate()方法执行认证
    // 在authenticate()方法的执行过程中
    // Spring Security会自动调用UserDetailsService对象的loadUserByUsername()获取用户详情
    // 并根据loadUserByUsername()返回的用户详情自动验证是否启用、判断密码是否正确等
    Authentication authentication
            = new UsernamePasswordAuthenticationToken(
                    adminLoginInfoDTO.getUsername(),
                    adminLoginInfoDTO.getPassword());
    authenticationManager.authenticate(authentication);
}

提示:此步骤不便于测试。

AdminController中添加处理登录认证的请求的方法:

@PostMapping("/login")
public JsonResult<Void> login(AdminLoginInfoDTO adminLoginInfoDTO) {
    adminService.login(adminLoginInfoDTO);
    return JsonResult.ok();
}

最后,还需要在Security的配置类中,将/admins/login添加到”白名单“中。

此时,可以通过Knife4j的在线API文档的调试功能尝试登录。

为了更好的显示错误信息,还应该对相关异常进行处理!首先,在ServiceCode中添加新的业务状态码:

public enum ServiceCode {

    OK(20000),
    ERR_BAD_REQUEST(40000),
    ERR_UNAUTHORIZED(40100), // 新增
    ERR_UNAUTHORIZED_DISABLED(40110), // 新增
    ERR_FORBIDDEN(40300), // 新增
    
    // ... ...(原有其它代码)

并且,在全局异常处理器(GlobalExceptionHandler)中处理新的异常:

@ExceptionHandler({
        InternalAuthenticationServiceException.class,
        BadCredentialsException.class
})
public JsonResult<Void> handleAuthenticationException(AuthenticationException e) {
    log.debug("处理AuthenticationException");
    log.debug("异常类型:{}", e.getClass().getName());
    log.debug("异常信息:{}", e.getMessage());
    Integer serviceCode = ServiceCode.ERR_UNAUTHORIZED.getValue();
    String message = "登录失败,用户名或密码错误!";
    return JsonResult.fail(serviceCode, message);
}

@ExceptionHandler
public JsonResult<Void> handleDisabledException(DisabledException e) {
    log.debug("处理DisabledException");
    Integer serviceCode = ServiceCode.ERR_UNAUTHORIZED_DISABLED.getValue();
    String message = "登录失败,此账号已经禁用!";
    return JsonResult.fail(serviceCode, message);
}

注意:此时,项目已经可以判断用户名、密码是否正确,但是,即使使用了正确的用户名、密码,且服务器响应的state20000,也并不是真正意义上的登录成功!因为Session中根本没有当前用户的认证信息!所以,即使登录成功,再去访问那些不在”白名单“中的URL,仍会响应403错误!

1.6. 关于Token与JWT

Token:票据,令牌。

当用户尝试登录,将请求提交到服务器端,如果服务器端认证通过,会生成一个Token数据并响应到客户端,此Token是有意义的数据,此客户端在后续的每一次请求中,都应该携带此Token数据,服务器端通过解析此Token来识别用户身份!

关于Session与Token:Session默认是保存在服务器的内存中的数据,会占用一定的服务器内存资源,并且,不适合集群或分布式系统(虽然可以通过共享Session来解决),客户携带的Session ID只具有唯一性的特点(理论上),不具备数据含义……而Token的本质是将有意义的数据进行加密处理后的结果,各服务器都只需要具有解析这个加密数据的功能即可获取到其中的信息含义,理论上不占用内存资源,更适用于集群和分布式系统,但是,存在一定的被解密的风险(概率极低)。

JWT = JSON Web Token,是使用JSON格式表示多项数据的Token。

在使用JWT之前,需要在项目中添加相关的依赖,用于生成JWT和解析JWT,例如添加:

<!-- JJWT(Java JWT) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

提示:更多依赖项可参考 https://jwt.io/libraries?language=Java

一个原始的JWT数据应该包含3个部分:

HEADER:ALGORITHM & TOKEN TYPE(算法与Token类型)

{
  "alg": "HS256",
  "typ": "JWT"
}

PAYLOAD(载荷):DATA

此部分的数据是自定义的,可按需存入任何所需的数据。

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

VERIFY SIGNATURE(验证签名)

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

使用jjwt生成和解析JWT数据的示例:

package cn.tedu.csmall.passport;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTests {

    // Secret Key
    String secretKey = "97iuFDVDfv97iuk534Tht3KJR89kBGFSBgfds";

    @Test
    public void testGenerate() {
        // 准备Claims值
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 9527);
        claims.put("name", "LiuLaoShi");
        claims.put("nickname", "JavaCangLaoShi");

        // JWT的过期时间
        Date expiration = new Date(System.currentTimeMillis() + 5 * 60 * 1000);
        System.out.println("过期时间:" + expiration);

        // JWT的组成:Header(头:算法和Token类型)、Payload(载荷)、Signature(签名)
        String jwt = Jwts.builder()
                // Header
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                // Payload
                .setClaims(claims)
                .setExpiration(expiration)
                // Signature
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        System.out.println("JWT=" + jwt);

        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiTGl1TGFvU2hpIiwiaWQiOjk1MjcsImV4cCI6MTY2MjQ1NDg5NH0.mHYjK70qenmqmQ5_NrjZsh2P0t-QPKvBedVDRqH2ed8
        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiTGl1TGFvU2hpIiwiaWQiOjk1MjcsImV4cCI6MTY2MjQ1NTA0NH0._7o_k9s3we-Ti-9rO4FpYzWxPxNDTFaLbAjZz-bOa8M

        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
        // .
        // eyJuYW1lIjoiTGl1TGFvU2hpIiwibmlja25hbWUiOiJKYXZhQ2FuZ0xhb1NoaSIsImlkIjo5NTI3LCJleHAiOjE2NjI0NTUwOTV9
        // .
        // KaiBd1LskHVPZzwfDdeoZOCHQ4FB-P_69at0g-1jyqs
    }

    @Test
    public void testParse() {
        // 注意:必须使用相同secretKey生成的JWT,否则会解析失败
        // 注意:不可以使用过期的JWT,否则会解析失败
        // 注意:复制粘贴此JWT时,不要带“尾巴”,否则会解析失败
        // 注意:不可以恶意修改JWT中的任何字符,否则会解析失败
        String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiTGl1TGFvU2hpIiwibmlja25hbWUiOiJKYXZhQ2FuZ0xhb1NoaSIsImlkIjo5NTI3LCJleHAiOjE2NjI0NTY3ODN9.32MwkSbDz1ce4EvEKHFMCIjcQFUDZz6hn5MtAYr0njQ";

        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
        Integer id = claims.get("id", Integer.class);
        String name = claims.get("name", String.class);
        String nickname = claims.get("nickname", String.class);
        System.out.println("id = " + id);
        System.out.println("name = " + name);
        System.out.println("nickname = " + nickname);
    }

}

作业

完成以下功能的Mapper层、Service层、Controller层,最终,通过Knife4j的在线API文档可以测试访问:

\1. 根据id删除管理员,需添加业务规则:被操作数据必须存在

\2. 根据id启用管理员,需添加业务规则:被操作数据必须存在,此管理员必须处理“禁用”状态才允许“启用”

\3. 根据id禁用管理员,需添加业务规则:被操作数据必须存在,此管理员必须处理“启用”状态才允许“禁用”

\4. 根据id修改管理员的密码:此功能假定为“超级管理员”修改其他管理员的密码,因此,直接填值即可,不需要验证原密码,新密码需要经过加密处理再存入到数据库中,需添加业务规则:被操作数据必须存在

\5. 根据id修改管理员的基本资料:包括nickname, description即可,需添加业务规则:被操作数据必须存在

此作业需在9月8日(周四)晚23:00前提交到作业系统

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 10:59:35  更:2022-09-13 11:04:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:30:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码