一、Spring Security对接到RBAC

对接原理

通过UserDetailsService和UserDetails这两个接口为Spring Security提供认证数据。

对接原理

用户登录之后先认证,通过认证管理器访问访问service层

首先创建VO类实现用户登录中信息的传递:

/**
 * @document: 管理员登录信息的VO类 向页面返回登录的管理员信息
 * @Author:SmallG
 * @CreateTime:2023/9/27+9:08
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AdminLoginInfoVO implements Serializable {
    /* 数据ID */
    private Long id;
    /* 用户名 */
    private String username;
    /* 密码(密文)*/
    private String password;
    /* 是否启用,1=启用,0=禁用 */
    private Integer enable;
    /* 权限列表 */
    private List<String> permissions;
    /* 角色列表 */
    private List<String> roles;
}

adminMapper接口中写用户登录的方法:

/**
 * 根据用户名查询管理员登录信息
 * @param username 用户名
 * @return 匹配登录信息,如果没有匹配的数据返回null
 */
AdminLoginInfoVO getLoginInfoByUsername(String username);

编写sql语句:

<!-- 对象关系映射 -->
<resultMap id="LoginInfoResultMap" type="cn.highedu.csmall.pojo.vo.AdminLoginInfoVO">
    <id column="id" property="id"></id>
    <result column="username" property="username"></result>
    <result column="password" property="password"></result>
    <result column="enable" property="enable"></result>
    <!--一对多-->
    <collection property="roles" ofType="String">
        <!--相当于调用String类的构造方法 创建String对象-->
        <constructor>
            <!-- 创建String对象传递的参数  角色名-->
            <arg column="name"/>
        </constructor>
    </collection>
    <collection property="permissions" ofType="String">
        <constructor>
            <!-- 创建String对象传递的参数 权限值-->
            <arg column="value"/>
        </constructor>
    </collection>
</resultMap>

<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
    SELECT ams_admin.id,
           ams_admin.username,
           ams_admin.password,
           ams_admin.enable,
           ams_role.name,
           ams_permission.value
        FROM ams_admin
             left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id
             left join ams_role on ams_admin_role.role_id = ams_role.id
             left join ams_role_permission on ams_role.id = ams_role_permission.role_id
             left join ams_permission on ams_role_permission.permission_id = ams_permissio.id
    WHERE ams_admin.username = #{username}
</select>

实现UserDetails接口:

/**
 * @document: 封装登录管理员的信息
 * @Author:SmallG
 * @CreateTime:2023/9/27+10:55
 */

@Slf4j
public class UserDetailsImpl implements UserDetails {

    //定义一个属性
    private AdminLoginInfoVO admin;
    //封装用户信息
    private List<GrantedAuthority> authorities = new ArrayList<>();

    //定义构造方法 用于封装角色和权限
    public UserDetailsImpl(AdminLoginInfoVO admin) {
        this.admin = admin;
        //读取角色和权限
        List<String> roles = admin.getRoles();
        List<String> permissions = admin.getPermissions();
        //把角色和权限封装到authorities中
        if (roles != null) {
            for (String role : roles) {
                //角色以ROLE_作为前缀
                authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
            }
        }
        if (permissions != null) {
            for (String permission : permissions) {
                //权限没有后缀,可以不用加
                authorities.add(new SimpleGrantedAuthority(permission));
            }
        }
    }

    //返回角色和权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return admin.getPassword();
    }

    @Override
    public String getUsername() {
        return admin.getUsername();
    }

    // 返回false表示用户过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //false表示锁定(例如三次输错密码锁定账户) true表示用户不被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //false表示过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //false表示禁用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

实现UserDetailsService接口:

/**
 * @document: 查询管理员信息
 * @Author:SmallG
 * @CreateTime:2023/9/27+10:55
 */

@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private AdminMapper adminMapper;

    //用于查询用户登录信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("Spring Security查询用户信息,参数:{}", username);
        AdminLoginInfoVO adminLoginInfoVO = adminMapper.getLoginInfoByUsername(username);
        if (adminLoginInfoVO != null) {
            return new UserDetailsImpl(adminLoginInfoVO);
        }
        throw new UsernameNotFoundException("用户名不存在");
    }
}

重启服务

Sercurity生效

二、跨域验证与JWT

1、跨域

前后端分离项目中,端口号不同,需要跨域认证才能彼此访问。

2、JWT

JSON Web Token,是一种用于进行身份认证和授权的开放标准,基于JSON格式的数据结构,在网络应用中被广泛使用。

JWT通常由三个部分组成,分别为头部(Header)、载荷(Payload)签名(Signature)

JWT不能存敏感信息,比如手机号、身份证号、密码等

JWT工作流程

流程简述:

  • 用户向服务器发送登录请求,服务器进行身份验证。
  • 验证成功后,服务器生成一个JWT,并将其发送给客户端。
  • 客户端将JWT保存在本地,并在后续的请求中将其发送给服务器。
  • 服务器验证JWT的有效性,如果验证通过,则执行请求操作。

3、使用JWT

1、Header(头)

Header通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法。它是一个JSON对象

2、Payload(栽荷)

Payload也是一个JSON对象,包含了一些声明和数据。这些声明称为Claim,用于描述JWT令牌中包含的信息,例如用户ID、用户名、过期时间等。Claim分为三类:注册的声明、公共的声明和私有的声明。

3、Signature

Signature用于验证JWT的真实性,防止JWT被篡改。Signature的计算需要使用Header和Payload中的数据以及一个密钥(secret)。计算步骤如下:

  • 使用Base64Url编码后的Header和Base64Url编码后的Payload使用句号(.)连接起来,得到一个字符串。
  • 使用Header中指定的签名算法对该字符串进行签名,得到一个签名值。
  • 使用Base64Url编码将签名值转换为字符串,得到Signature。

4、JJWT API

(1)引入依赖

<!-- 添加JJWT依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!-- JJWT 需要依赖 JAXB -->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

(2)Token测试

@Slf4j
public class TokenTest {
    //定义签名钥匙
    private String SECRET_KEY = "123456789012345678901234567890";

    /**
     * 用于生成token
     *
     * @return
     */
    private String generateToken() {
        //定义当前日期
        Date now = new Date();
        //定义过期时间 7天
        Date expireDate = new Date(now.getTime() + 1000 * 60 * 60 * 24 * 7);
        //定义一个token
        String token = Jwts.builder()
                .setIssuedAt(now)    //设置签发时间
                .setExpiration(expireDate)  //过期时间
                .setSubject("admin") //面向用户
                .claim("id", "666666")  //设置自定义内容 自定义要求
                .claim("roles", "admin,manager,user")
                .claim("permissions", "user:list,user:add,user:delete,user:update")
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)  //设置签名算法 (加密算法,自定义钥匙)
                .compact(); //生成token
        log.info("token:{}", token);
        return token;
    }

    //解析Token
    private void parseToken(String token) {
        //解析Token
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY) //设置钥匙
                .parseClaimsJws(token) //设置及诶西的token
                .getBody();
        //获取token中的信息
        Date issuedAt = claims.getIssuedAt();
        //获取过期时间
        Date expiration = claims.getExpiration();
        String subject = claims.getSubject();//面向的用户
        String id = claims.get("id",String.class);//获取自定义信息
        String roles = claims.get("roles",String.class);
        String permissions = claims.get("permissions",String.class);
        log.info("签发时间:{}", issuedAt);
        log.info("过期时间:{}", expiration);
        log.info("面向的用户:{}", subject);
        log.info("id:{}", id);
        log.info("角色:{}", roles);
        log.info("权限:{}", permissions);
    }

    @Test
    public void test() {
      String token = generateToken();
        parseToken(token);
    }
}

(3)在application-dev里配置token的钥匙和有效期:

# 配置token的钥匙和有效期
jwt:
  secret-key: coolshark_key_1234567890
  duration-in-minute: 60

(4)创建Service

Service用于生成和解析Token,在解析token出错时,应该也有个状态码,所以在ServiceCode中添加解析错误的状态码:

/**
 * 错误:JWT过期
 */
ERR_JWT_EXPIRED(60000),
/**
 * 错误:验证签名失败
 */
ERR_JWT_SIGNATURE(60100),
/**
 * 错误:JWT格式错误
 */
ERR_JWT_MALFORMED(60200),
/**
 * 错误:JWT已退出
 */
ERR_JWT_LOGOUT(60300),
/**
 * 未知错误
 */
ERR_UNKNOWN(99999);

创建service:

/**
 * @document: 生成和解析Token
 * 例如把登录用户AdminLoginInfoVO保存在Token中
 * 把Token解析为AdminLoginInfoVO对象
 * @Author:SmallG
 * @CreateTime:2023/9/27+14:06
 */

@Service
@Slf4j
public class JwtTokenService {
    @Value("${jwt.secret-key}")
    private String secretKey;

    @Value("${jwt.duration-in-minute}")
    private Long durationInMinute;

    /**
     * 把UserDetailsImpl对象转换为JWT字符串
     *
     * @param userDetails
     * @return
     */
    public String createToken(UserDetailsImpl userDetails) {
        AdminLoginInfoVO adminLoginInfoVO = userDetails.getAdmin();
        return createToken(adminLoginInfoVO);
    }


    /**
     * 把JWT字符串解析为UserDetailsImpl对象
     * @param jwt
     * @return
     */
    public UserDetailsImpl parseJwtToken(String jwt){
        AdminLoginInfoVO adminLoginInfoVO = parseToken(jwt);
        return new UserDetailsImpl(adminLoginInfoVO);
    }

    /**
     * 把adminLoginInfoVO对象转换为JWT字符串
     *
     * @param adminLoginInfoVO
     * @return
     */
    public String createToken(AdminLoginInfoVO adminLoginInfoVO) {
        log.info("开始创建JWT,参数:{}", adminLoginInfoVO);
        String subject = adminLoginInfoVO.getUsername();
        Date now = new Date();
        Date exp = new Date(now.getTime() + durationInMinute * 60 * 1000);//转换成毫秒
        Long id = adminLoginInfoVO.getId();
        Integer enable = adminLoginInfoVO.getEnable();
        //把集合中的数据取出来,中间用逗号隔开
        String roles = String.join(",", adminLoginInfoVO.getRoles());
        String permissions = String.join(",", adminLoginInfoVO.getPermissions());
        //创建JWT字符串
        String jwt = Jwts.builder().setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(exp)
                .claim("id", id)
                .claim("enable", enable)
                .claim("roles", roles)
                .claim("permissions", permissions)
                .signWith(SignatureAlgorithm.HS256, secretKey) //算法加钥匙
                .compact();
        log.info("创建JWT成功,结果:{}", jwt);
        return jwt;
    }

    /**
     * 把JWT字符串解析为AdminLoginInfoVO对象
     *
     * @param jwt
     * @return
     */
    public AdminLoginInfoVO parseToken(String jwt) {
        log.info("开始解析JWT,参数:{}", jwt);
        try {
            Claims claims = Jwts.parser().setSigningKey(secretKey)
                    .parseClaimsJws(jwt).getBody();
            //从JWT中获取管理员信息
            Long id = Long.valueOf(claims.get("id").toString());
            String username = claims.getSubject();
            Integer enable = Integer.valueOf(claims.get("enable").toString());
            String roles = claims.get("roles").toString();
            String permissions = claims.get("permissions").toString();
            //创建adminLoginInfoVO对象
            AdminLoginInfoVO adminLoginInfoVO = new AdminLoginInfoVO();
            adminLoginInfoVO.setId(id);
            adminLoginInfoVO.setUsername(username);
            adminLoginInfoVO.setEnable(enable);
            adminLoginInfoVO.setRoles(Arrays.asList(roles.split(",")));
            adminLoginInfoVO.setPermissions(Arrays.asList(permissions.split(",")));
            log.info("解析JWT成功,结果:{}", adminLoginInfoVO);
            return adminLoginInfoVO;
        } catch (ExpiredJwtException e) {
            log.error("JWT已过期", e);
            throw new ServiceException(ServiceCode.ERR_JWT_EXPIRED, "JWT已过期");
        } catch (SignatureException e) {
            log.error("解析JWT失败");
            throw new ServiceException(ServiceCode.ERR_JWT_SIGNATURE, "令牌无效");
        } catch (Exception e) {
            log.error("解析JWT失败");
            throw new ServiceException(ServiceCode.ERR_JWT_MALFORMED,"格式错误");
        }
    }

}

编写测试类对以上代码进行测试:

@Slf4j
@SpringBootTest
class JwtTokenServiceTest {
    @Autowired
    private JwtTokenService jwtTokenService;

    @Autowired
    private AdminMapper adminMapper;

    @Test
    public void test(){
        //登录用户
        AdminLoginInfoVO adminLoginInfoVO = adminMapper.getLoginInfoByUsername("root");
        String jwt = jwtTokenService.createToken(adminLoginInfoVO);
        log.info("Token:{}",jwt);
        AdminLoginInfoVO admin = jwtTokenService.parseToken(jwt);
        log.info("解析的对象:{}",admin);
    }
}

测试结果:

生成和解析JWT测试效果

(5)Spring Security 自定义登录

在WebSecurityConfig中配置类中定义一个AuthenticationManager的Bean:

@Configuration
@EnableWebSecurity //开启登录认证授权
public class WebSecurityConfig {

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager(); //返回认证管理器
    }
}

创建JwtVO

/**
 * @document: 包含Token的VO对象
 * @Author:SmallG
 * @CreateTime:2023/9/27+15:20
 */
@Data
@AllArgsConstructor
public class JwtVO {
    private String token;
}

创建AdminLoginInfoDTO,用于返回页面的登录信息:

@Data
public class AdminLoginInfoDTO implements Serializable {
    private String username;
    private String password; //明文
}

在IadminService中添加登录成功返回JWT令牌的方法

/**
 * 前端通过表单登录 返回token信息
 * @param adminLoginInfoDTO 封装了用户登录信息对象
 * @return 返回JWT令牌(登录成功)
 */
JwtVO login(AdminLoginInfoDTO adminLoginInfoDTO);

实现方法:

@Override
public JwtVO login(AdminLoginInfoDTO adminLoginInfoDTO) {
    //1、创建认证对象,传入用户名和密码
    UsernamePasswordAuthenticationToken token =
            new UsernamePasswordAuthenticationToken(adminLoginInfoDTO.getUsername(), adminLoginInfoDTO.getPassword());
    //2、调用认证管理器的方法,传入认证对象(主要传入用户名和密码)
    Authentication authenticate = authenticationManager.authenticate(token);

    //3、如果认证成功会将认证信息存入上下文对象(SecurityContext)服务只要不关闭,这个上下文对象数据一直存在
    //如果没有存储信息 说明认证失败
    SecurityContextHolder.getContext().setAuthentication(authenticate);
    //获取认证的主体
    Object principal = authenticate.getPrincipal();
    //认证的主体是UserDetails的实现类 UserDetailsImpl类的对象
    UserDetailsImpl userDetails = (UserDetailsImpl) principal;
    //4、认证成功生成token
    String tokenString = jwtTokenService.createToken(userDetails);
    //5、把token返回给客户端
    return new JwtVO(tokenString);
}

在AdminController里面写一个登录方法:

@PostMapping("/login")
public JsonResult<JwtVO> login(@RequestBody AdminLoginInfoDTO adminLoginInfoDTO) {
    log.info("开始处理【管理员登录】的请求,参数:{}", adminLoginInfoDTO);
    JwtVO token = adminService.login(adminLoginInfoDTO);
    return JsonResult.ok(token);
}

在AuthController写登录时获取用户名和头像的信息:

/**
 * 获取当前登录管理员的信息(用户名、头像)
 * @AuthenticationPrincipal 从上下文对象获取UserDetailsImpl对象
 * @return
 */
@GetMapping("/info")
public JsonResult<AdminLoginInfoVO> getInfo(@AuthenticationPrincipal UserDetailsImpl userDetails) {
    //根据token查询管理员信息
    log.info("开始处理【根据token查询管理员信息】的请求,参数:{}",userDetails);
    AdminLoginInfoVO adminLoginInfoVO = userDetails.getAdmin();
    log.info("管理员信息:{}",adminLoginInfoVO);
    return JsonResult.ok(adminLoginInfoVO);
}

在WebSecurityConfig中设置授权认证规则:

@Configuration
@EnableWebSecurity //开启登录认证授权
public class WebSecurityConfig {

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager(); //返回认证管理器
    }

    /**
     * 设置URL授权规则
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //不需要认证的url
        String[] permitAllUrls = {
            "/auth/**",
            "/error/**"
        };
        //配置不需要认证的请求路径
        http.authorizeHttpRequests((authorization ->
                authorization.requestMatchers(permitAllUrls).permitAll()));
        //配置需要认证的请求路径
        http.authorizeHttpRequests(authorization->
                authorization.anyRequest().authenticated());
        //禁用跨域防护机制(不推荐)
        http.csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }
}

5、利用过滤器检验JWT

主要用于检查请求是否包含有效的JWT令牌,以验证用户身份。

JWT认证过滤器的主要功能如下:

  • 检查请求是否包含有效的JWT令牌:JWT认证过滤器会检查每个请求是否包含有效的JWT令牌。如果请求中没有包含JWT令牌或者JWT令牌无效,则会返回一个未经授权的错误响应。
  • 解码JWT令牌并验证其有效性:JWT认证过滤器会解码请求中的JWT令牌,并使用密钥对其进行验证。如果JWT令牌无效,则会返回一个未经授权的错误响应。
  • 将解码后的用户信息存储到Spring Security上下文中:如果JWT令牌有效,则JWT认证过滤器会将从JWT令牌中解码的用户信息存储到Spring Security的上下文中,以便后续的访问授权使用。

(1)验证Token过滤器

创建filter包中新建JwtAuthenticationFilter类:

/**
 * @document: JWT认证过滤器
 * 在发送请求的过程中 一般把token放在请求头中
 * @Author:SmallG
 * @CreateTime:2023/9/27+16:36
 */

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenService jwtTokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头
        String jwtToken = request.getHeader("Authorization");
        log.info("jwtToken:{}", jwtToken);
        //不需要认证的请求,直接访问 例如登录、注册、验证码等
        if (jwtToken == null) {
            filterChain.doFilter(request, response);
            return;
        }
        //认证
        try {
            //从token中获取信息
            UserDetailsImpl userDetails = jwtTokenService.parseJwtToken(jwtToken);
            log.info("userDetails:{}", userDetails);
            //创建UsernamePasswordAuthenticationToken对象存储认证结果
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            //UsernamePasswordAuthenticationToken对象存储在上下文对象中
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //放行
            filterChain.doFilter(request, response);
        } catch (ServiceException e) {
            log.info("token无效");
            //响应错误消息(一段json代码)
            response.setContentType("application/json; charset=utf-8");
            JsonResult<Void> jsonResult = JsonResult.fail(e);
            String json = new ObjectMapper().writeValueAsString(jsonResult);
            //把json数据写入到页面中
            response.getWriter().println(json);
        }
    }
}

在WebSecurityConfig中配置JWT认证过滤器:(注入和配置)

@Configuration
@EnableWebSecurity //开启登录认证授权
public class WebSecurityConfig {
--->@Autowired
--->private JwtAuthenticationFilter jwtAuthenticationFilter;

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager(); //返回认证管理器
    }

    /**
     * 设置URL授权规则
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //不需要认证的url
        String[] permitAllUrls = {
            "/auth/**",
            "/error/**"
        };
        //配置不需要认证的请求路径
        http.authorizeHttpRequests((authorization ->
                authorization.requestMatchers(permitAllUrls).permitAll()));
        //配置需要认证的请求路径
        http.authorizeHttpRequests(authorization->
                authorization.anyRequest().authenticated());

------->//配置jwt过滤器
------->http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //禁用跨域防护机制(不推荐)
        http.csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }
}

JWT登录测试:

POST http://localhost:9081/auth/login
Content-Type: application/json

{
  "username": "root",
  "password": "1234"
}

登录测试

(2)处理未认证403

创建JWT认证失败处理器

/**
 * @document: JWT认证失败处理器(解决客户端403错误)
 * 1、未登录时访问受保护的资源
 * 2、认证过程中出现异常
 * @Author:SmallG
 * @CreateTime:2023/9/27+17:05
 */
@Component
@Slf4j
public class JwtTokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException e)
            throws IOException, ServletException {
        log.info("认证失败信息:{}", e);
        response.setStatus(HttpServletResponse.SC_OK); //状态码200,不至于让客户看403
        response.setContentType("application/json; charset=utf-8");
        JsonResult<Void> jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, "请登录后再访问");
        //转换成json,再把json输出出去
        String json = new ObjectMapper().writeValueAsString(jsonResult);
        response.getWriter().println(json);
    }
}

然后在WebSecurityConfig 中注入并且进行配置:

@Configuration
@EnableWebSecurity //开启登录认证授权
public class WebSecurityConfig {
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

--->@Autowired
--->private JwtTokenAuthenticationEntryPoint jwtTokenAuthenticationEntryPoint;

    //认证管理器
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager(); //返回认证管理器
    }

    /**
     * 设置URL授权规则
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //不需要认证的url
        String[] permitAllUrls = {
            "/auth/**",
            "/error/**"
        };
        //配置不需要认证的请求路径
        http.authorizeHttpRequests((authorization ->
                authorization.requestMatchers(permitAllUrls).permitAll()));
        //配置需要认证的请求路径
        http.authorizeHttpRequests(authorization->
                authorization.anyRequest().authenticated());

        //配置jwt过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

------->//配置认证失败处理器
------->http.exceptionHandling().authenticationEntryPoint(jwtTokenAuthenticationEntryPoint);
        //禁用跨域防护机制(不推荐)
        http.csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }
}

403拦截

(3)获取当前用户信息

注解@AuthenticationPrincipal,可以用来在控制器方法中注入当前已登录系统的用户信息,这个信息实际上是存储在Security Context中的已认证的主体信息UserDetailsImpl,包括了用户的各种信息和权限等。

下面是一个示例控制器方法,演示如何使用认证主体获取当前用户的信息:

   @GetMapping("/info")
    public JsonResult<AdminStandardVO> getInfo(
            @AuthenticationPrincipal UserDetailsImpl userDetails) {
        log.debug("开始处理【根据token查询管理员信息】的请求,参数:{}", userDetails);
        AdminLoginInfoVO adminLoginInfoVO = userDetails.getAdmin();
        AdminStandardVO admin = adminService.getAdminById(adminLoginInfoVO.getId());
        log.debug("管理员信息:{}", admin);
        return JsonResult.ok(admin);
    }

请使HTTP客户端进行测试:

GET http://localhost:9081/auth/info

6、将JWT认证整合到界面

jwt图片也需要认证

(1)配置认证通信接口

在前端项目的 src/api/user.js 文件中,定义了与后端的通信接口,其中包括登录、获取用户信息和退出登录等功能。这些接口用于与后端控制器进行对接,以便实现登录和用户信息获取等操作。

import request from '@/utils/request'
export function login(data) {
  return request({
    url: '/auth/login',
    method: 'post',
    data
  })
}
export function getInfo(token) {
  return request({
    url: '/auth/info',
    method: 'get',
    params: { token }
  })
}
export function logout() {
  return request({
    url: '/auth/logout',
    method: 'post'
  })
}

(2)整合前端登录功能

为了满足后端的要求,在用户登录后的每次请求中都必须携带Authorization请求头,需要在前端的 src/utils/request.js 文件中进行相应的设置。这个文件是请求和响应通信协议的拦截器,可以用于设置统一的请求参数和处理统一的响应结果。

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent
    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['Authorization'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

在响应拦截器src/utils/request.js中,处理未认证的错误码,当出现未认证的情况时,将用户重定向到登录页面:

 // 40100 登录失败,用户名或密码错
      // 40300 无权限
      // 60000 JWT已过期
      // 60100 验证签名失败
      // 60200 JWT格式错误
      if (res.code === 40100 || res.code === 40300 ||
          res.code === 60000 || res.code === 60100 || res.code === 60200) {
        // to re-login
        MessageBox.confirm('您已注销,点击取消停留在此页面,或重新登录', '确认登录', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }

修改认证,添加图像认证,在jwt认证过滤器中添加内容:

/**
 * @document: JWT认证过滤器
 * 在发送请求的过程中 一般把token放在请求头中
 * @Author:SmallG
 * @CreateTime:2023/9/27+16:36
 */

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenService jwtTokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头
        String jwtToken = request.getHeader("Authorization");
        log.info("jwtToken:{}", jwtToken);
------->新加入开始
        //Spring Security 6.0后 对于图片需要认证
        //从客户端的Cookie中获取token Cookie的名字是vue_admin_template_token
        String cookieName = "vue_admin_template_token";
        if (jwtToken == null) {
            String cookieHeader = request.getHeader("Cookie");//获取请求中的Cookie
            log.info("Cookie:{}", cookieHeader);
            if (cookieHeader != null) {
                String[] cookies = cookieHeader.split(";");
                for (String cookie : cookies) {
                    if (cookie.trim().startsWith(cookieName + "=")) {
                        //截取从这个名字到最后的字符串
                        jwtToken = cookie.substring(cookieName.length() + 1);
                        break; //找到后break,下次就不用做这个循环了
                    }
                }
            }
        }
------->新加入结束
        //不需要认证的请求,直接访问 例如登录、注册、验证码等
        //如果还是空那就是没登录
        if (jwtToken ==null){
            filterChain.doFilter(request,response);
            return;
        }
        //认证
        try {
            //从token中获取信息
            UserDetailsImpl userDetails = jwtTokenService.parseJwtToken(jwtToken);
            log.info("userDetails:{}", userDetails);
            //创建UsernamePasswordAuthenticationToken对象存储认证结果
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            //UsernamePasswordAuthenticationToken对象存储在上下文对象中
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //放行
            filterChain.doFilter(request, response);
        } catch (ServiceException e) {
            log.info("token无效");
            //响应错误消息(一段json代码)
            response.setContentType("application/json; charset=utf-8");
            JsonResult<Void> jsonResult = JsonResult.fail(e);
            String json = new ObjectMapper().writeValueAsString(jsonResult);
            //把json数据写入到页面中
            response.getWriter().println(json);

        }
    }
}

重启项目后可正常登录

最后修改:2023 年 09 月 28 日
如果觉得我的文章对你有用,请随意赞赏