本文介绍: Spring Security 功能强大的处理**认证**和**授权**的框架添加框架后,在不添加任何配置时,访问任何页面都会重定向框架自带login页面框架提供了默认用户名:“user“与密码:启动生成的“UUID“

Spring Security

功能强大的处理认证授权的框架

添加框架后,在不添加任何配置时,访问任何页面都会重定向到框架自带login页面

框架提供了默认用户名:user密码:启动时生成UUID

登陆成功后,不允许发送post请求

具有相似功能的框架: Shiro

Spring Security访问配置

使用需要进行相关配置使用配置类进行配置即可

配置需要继承WebSecurityConfigurerAdapter 类,选择重写其中的 configure(HttpSecurity http) 方法

重写访问页面将不需要登录自带登录登出页面失效post请求依然不可

@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
    }
}	

可以调用http参数中的某些方法来添加功能配置

其中可以配置url的授权访问

@Override
    protected void configure(HttpSecurity http) throws Exception {

        // 配置授权访问的权限
        http.authorizeRequests()
                .anyRequest() // 选择任何请求必须匹配某些条件
                .authenticated(); // 通过认证后才可以访问




        http.formLogin();// 添加loginlogout页面
    }

以上表达的是所有请求必须通过认证(登录)后才可以访问

也可以配置其他匹配条件进行操作

@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    String[] urls = {"","/","/index.html"};

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 配置授权访问的权限
        http.authorizeRequests()
                .mvcMatchers(urls) // 匹配某些请求
                .permitAll() // 不需要认证就可以访问
                .anyRequest() // 选择任何请求必须匹配某些条件
                .authenticated(); // 通过认证后
        http.formLogin();// 添加loginlogout页面
    }
}

注意:访问权限执行是从上至下,如果前面的配置已经匹配和通过条件,则下面的配置都不运行

上面完成效果是,如果请求是访问首页,则不需要认证即可访问,其他请求则需要认证才可访问

临时自定义账号

除了使用框架提供的默认账号密码,也可以自行添加自定义账号

使用自定义账号需要自定义类,并实现UserDetailsService接口,并确保此类是一个组件类,框架会基于实现类的某些规则,来处理认证

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	@Override
	public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    	log.debug("Spring Security自动调用loadUserByUsername()方法参数:{}", s);

	// 假设正确用户名root,匹配的密码是1234
	if (!"root".equals(s)) {
    	log.warn("用户名【{}】错误,将不会返回有效的UserDetails用户详情)", s);
    	return null;
	}

	UserDetails userDetails = User.builder() // 构建模式
        	.username("root") // 存入用户名
       	 	.password("1234") // 存入密码
       	    .disabled(false) // 存入启用禁用状态
       	    .accountLocked(false) // 存入账号是否锁定状态
      	    .credentialsExpired(false) // 存入凭证是否过期状态
        	.accountExpired(false) // 存入账号是否过期状态
        	.authorities("这是一个临时的山寨权限,暂时没什么用") // 存入权限列表
        	.build(); // 执行构建,得到UserDetails类型对象
	log.debug("即将向Spring Security返回UserDetails类型对象返回结果:{}", userDetails);
	return userDetails;

	}
}

项目存在UserDetailsService类型组件时,登录框架自动使用登录页面表单提交用户名调用loadUserByUsername(String s)方法得到UserDetails类型对象,此对象应该包含用户的相关信息例如密码账号状态等,接下来,Spring Security会自动判断账号的状态,并使用登录表单提交过来的密码UserDetails中的密码进行对比,以决定此账号是否能够登录。

所以,当重写loadUserByUsername()时,只需要实现“根据用户名返回匹配的用户详情即可,至于此方法调用、返回的结果如何用于判断是否能够成功登录,都是由Spring Security自动处理的!

Spring Security在验证登录时,要求密码必须经过加密处理,即:在loadUserByUsername()方法中返回的UserDetails中的密码必须是密文,即使你执意不加密,也必须明确的表示出来!

SecurityConfiguration类中通过@Bean方法来配置PasswordEncoder,并返回某个密码编码器对象,Spring Security会自动使用它来验证密码,例如,可以使用NoOpPasswordEncoder表示“不对密码进行加密处理”,例如

@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

提示:当项目存在UserDetailsService类型组件对象时,Spring Security框架不再提供默认账号(用户名为user,密码为启动时的UUID值的账号),所以,启动项目时也不会看到临时的UUID密码了。

以上用户信息(usernamepassword等)在实际用途中可以修改数据库查找出来的信息,这样就能实现用户输入数据数据库中的信息进行比对进行验证

如果要比对数据库中的加密信息(例如:密码 存进数据库时需要加密处理),则需要弃用之前的不加密的密码编码器

目前最主流、安全性最高的加密算法 BCrypt 其主要特征有:

将之前的不加密编码器换为BCrypt编码器:

@Bean
public PasswordEncoder passwordEncoder() {
    // return NoOpPasswordEncoder.getInstance();
    return new BCryptPasswordEncoder();
}

解决post请求报403错误问题

403错误网站访问过程中,常见错误提示资源不可用,服务器理解客户的请求,但拒绝处理它。通常由于服务器文件目录权限设置导致。

这是因为在Spring Security框架中为了解决伪造的跨域攻击

**伪造的跨域攻击:**此类攻击基于服务器客户端浏览器信任”,例如,用户在浏览器的第1个选项卡中登录了,那么,在第2个、第3个等等同一个浏览器的其它选项卡中访问同样的服务器,也会被视为“已登录”的状态。所以,假设某个用户在浏览器的第1个选项卡中登录了网上银行,此用户在第2个选项卡中打开了另一个网站,此网站可能恶意网站(不是此前第1个选项卡的网上银行网站),在恶意网站中隐藏了一个向网上银行的网站发起请求的链接,并自动发出了请求(比较典型的做法是将链接设置<img>标签src属性值,并隐藏<img>标签使之不显示),则会导致在第2个选项卡中的恶意网站被打开时,就自动的向网上银行发起了请求,而网上银行收到了请求后,会视为“已登录”的状态!

框架对此有一个解决方案

服务器会要求客户端在发送表单额外返回服务器提前生成的一串UUID,框架自带表单中已经从服务器获取了该UUID,所以登陆时不会报错,但是自己编写的页面中没有此UUID所以会造成post请求报403错误,但是在前后分离的项目中此类解决方法不适用

如果想使用post请求需要在配置类禁用框架的此功能

@Override
    protected void configure(HttpSecurity http) throws Exception {

       

        http.csrf().disable();// 禁用跨站请求伪造解决方案


    }

解决伪造的跨域攻击问题可以使用其他方法

xxxxxxxx

前后分离的登录(认证)

由于框架自带的登录页面是存在服务器的,无法实现前后分离设计,所以需要先不启用框架自带的登录页面,改用自己编写页面发请求

接收一个新的请求时,如果需要使其不认证直接访问,需要在配置类中的“白名单”中添加

@Override
    protected void configure(HttpSecurity http) throws Exception {

        // 无需认证访问"白名单"
        String[] urls = {
                "",
                "/",
                "/index",
                "/admins/login" // 登录页面
        };

        http.csrf().disable();

        // 配置授权访问的权限
        http.authorizeRequests()
                .mvcMatchers(urls) // 匹配某些请求
                .permitAll() // 不需要认证就可以访问
                .anyRequest() // 选择任何请求必须匹配某些条件
                .authenticated(); // 通过认证后

        // http.formLogin();// 启用loginlogout页面
    }

然后编写controller中的代码

@RestController
@RequestMapping("/admins")
public class AdminController {

    @Autowired
    IAdminService adminService;

    @PostMapping("/login")
    public JsonResult login(AdminLoginDTO adminLoginDTO){
        System.out.println("调用service进行处理");
        adminService.login(adminLoginDTO);
        return JsonResult.ok();
    }
}

然后编写service的代码

@Service
public class AdminServiceImpl implements IAdminService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void login(AdminLoginDTO adminLoginDTO) {
        log.debug("开始处理验证登录的业务");

        Authentication authentication = new UsernamePasswordAuthenticationToken(
                adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
        );

        authenticationManager.authenticate(authentication); // 调用认证管理器
        log.debug("验证登录成功");
    }
}

service验证过程中需要使用AuthenticationManager接口,接口类型的对象可以通过Security配置类中重写父类方法得到 (方法上加@Bean注解使其自动添加到框架的容器中)

配置类中添加:

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

调用authenticationManager.authenticate(authentication)方法是需要一个参数这个参数包含的的是浏览器输入的用户名和密码,验证时框架会调用之前写过的实现了UserDetailsService接口的UserDetailsServiceImpl类中的loadUserByUsername方法,而此方法中的userDetails对象则是数据库中的用户信息,此时就可以完成浏览传递数据的验证,通过就是认证成功!

401和403

401和403的主要区别在于

授权

完成以上认证步骤后,可以提示认证成功,但是在做出其他访问时依然会提示403错误,这是因为上述认证步骤只是完成数据的验证,没有将认证结果保存,所以需要进行相关操作

可以使用Spring Security中的SecurityContext获取或者设置保存认证信息,SecurityContext默认基于Session的,所以,也符合Session的相关特征例如默认有效期(过期时间)。

在Spring Security中为每个客户端分配了一个SecurityContext,并且Security会根据在SecurityContext中是否存在有效Authenication判断是否已经通过认证

  • 如果在SecurityContext存在有效的Authentication:已通过认证
  • 如果在SecurityContext存在有效的Authentication:未通过认证

通过SecurityContextHolder静态方法getContext方法即可得到当前客户端对应的SecurityContext对象

@Service
public class AdminServiceImpl implements IAdminService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void login(AdminLoginDTO adminLoginDTO) {
        log.debug("开始处理验证登录的业务");

        Authentication authentication = new UsernamePasswordAuthenticationToken(
                adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
        );

        Authentication authenticationResult = // 新增
                authenticationManager.authenticate(authentication); 
        log.debug("验证登录成功,即将向SecurityContext中存入Authentication"); // 新增
        SecurityContext securityContext = SecurityContextHolder.getContext(); // 新增
        securityContext.setAuthentication(authenticationResult); // 新增
    }
}

之前调用的authenticationManager.authenticate()方法有一个Authentication类型返回值,使用此类型可以.getContext获取容器对象,调用容器对象的.setContext()给容器中添加authenticationManager.authenticate()方法的返回值

自定义UserDetails对象

如果当事人信息缺少逻辑相关的必须信息,例如 id和之前的enable的信息

创建一个类继承与UserDetail或它的子类,在构造方法中调用超类的全参构造方法

@ToString(callSuper = true)
public class AdminDetails extends User {

    @Getter
    private Long id;

    public AdminDetails(Long id,String username, String password, boolean enabled,Collection<? extends GrantedAuthority> authorities) {
        // 这里由于不需要关心boolean credentialsNonExpired, boolean accountNonLocked的值
        // 所以直接传入一个固定的true
        // 													↓此数据为权限列表
        super(username, password, enabled, true, true, true, authorities);
        this.id = id;
    }

}

同时,mapper层的查询也需要与 AdminDetails数据保持一致(例如)idenable权限列表信息

然后,调整UserDetailsServiceImpl类中loadUserByUsername()方法的返回结果改为返回AdminDetails类型的对象:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("Spring Security自动调用了loadUserByUsername()方法,参数:{}", s);

AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
if (loginInfo == null) {
    String message = "用户名不存在,将无法返回有效的UserDetails对象,则返回null";
    log.warn(message);
    return null;
}

log.debug("开始创建返回给Spring Security的UserDetails对象……");

// ========== 以下是新的代码替换了原有的代码 ==========
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("这是一个临时的山寨权限,暂时没什么用"));

AdminDetails adminDetails = new AdminDetails(
        loginInfo.getId(),
        loginInfo.getUsername(),
        loginInfo.getPassword(),
        loginInfo.getEnable() == 1,
        authorities
);

log.debug("即将向Spring Security返回UserDetails类型的对象,返回结果:{}", adminDetails);
return adminDetails;

}

此时从SecurityContext中获取到的Principal(当事人)信息就是现在添加的AdminDetails中的所有数据

修改之前controller中的

@GetMapping("/getPrincipal")
// 								注入的类型改为AdminDetails ↓↓↓↓↓↓↓↓↓↓↓↓		
    public String getPrincipal(@AuthenticationPrincipal AdminDetails adminDetails) {
        log.debug("当事人用户id:{}",adminDetails.getId());
        log.debug("当事人用户名是:{}",adminDetails.getUsername());
        return "接收到【查询列表】请求,但未开发";
    }

开启权限检查

在项目中不同角色具有不同的权限,这时需要开启权限的检查

开启的操作如下:

修改之前UserDetailsServiceImpl中的权限代码

@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 loginInfo = adminMapper.getLoginInfoByUsername(s);
    log.debug("根据用户名【{}】查询登录信息,结果:{}", s, loginInfo);
    if (loginInfo == null) {
        String message = "用户名不存在,将无法返回有效的UserDetails对象,则返回null";
        log.warn(message);
        return null;
    }

    // ===== 以下是本次调整的代码 ======
    // 将查询的权限列表信息添加
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    for (String permission : loginInfo.getPermissions()) {
        authorities.add(new SimpleGrantedAuthority(permission));
    }
    
    // 暂不关心后续的本次未调整的代码
}

}

经过以上调整后,在SecurityContext中的Authentication中的Principal中的权限,就是数据库中查出来的管理员的真实权限!

接下来,就可以利用Spring Security的检查权限的机制,需要先在配置类上开启基于方法的权限检查”,例如:

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    // 暂不关心方法内部的代码
}

注意:以上注解用于开启基于方法的权限检查,并不要求是哪个组件中的方法,也就是说,你可以把以上注解添加在项目中的任何方法上,例如Service中的方法、Controller中的方法,甚至其它组件中的方法,由于当前项目中,所有Service方法都是被Controller方法调用的,所以,推荐检查权限的注解添加在Controller中的方法上。

然后,在AdminController中:

@GetMapping("")
@PreAuthorize("hasAuthority('/ams/admin/read')") // 新增
public String list(@AuthenticationPrincipal @ApiIgnore 
    AdminDetails adminDetails) {
    // 暂不关心方法内部的代码
}

至此,权限的配置已经完成!如果使用无权限的账号发起请求,服务器端出现错误

org.springframework.security.access.AccessDeniedException: 不允许访问

Session的问题

服务器端程序通常是基于HTTP协议的,而HTTP协议是一种“无状态”的通信协议,所以,它并不能保存来访的客户端的状态,只是简单的“请求、响应”的处理而已!也就是说,当同一个客户端多次访问同一个服务器端时,服务器并不能识别来访的客户端就是前序曾经来访过的客户端!

开发实践中,是需要识别客户端身份的,所以,在编程技术上,可以使用Session机制来解决此问题

Session的本质存储服务器端内存中的一个K-V结构数据服务器端会为每一个来访的客户端的首次访问分配一个Session ID(本质上是一个UUID值,如果客户端的请求中没有携带Session ID,则服务器端生成并发回给客户端,如果客户端的请求中已经携带Session ID,则服务器端不会生成)此Session ID就是客户端访问服务器端的Session数据时使用的Key,所以,每个客户端在服务器上都有一份对应的Session数据(K-V中的Value)。

由于Session是存储在服务器端的内存中的数据内存是非常重要的,且容量相对较小的存储设备,所以,必须设置一些清除Session的机制默认的典型的清除机制就是“超时自动清除”,也就是说,某个客户端在最后一次提交请求后的多长时间内(常见超时时间是15分钟或30分钟没有再次提交请求,则服务器端会自动清除此客户端对应的Session数据。

由于Session是存储在服务器端的内存中的数据,所以,必然存在一些缺点:

JWT

Token令牌,票据

使用Token机制时,当客户端第1次向服务器提交请求时,或提交登录请求时,客户端直接发起请求,而服务器端会在验证登录成功后,生成此客户端对应的Token数据并响应到客户端,后续,客户端会携带此Token数据向服务器端发起请求,而服务器端会根据Token识别客户端的身份

在处理过程中,服务器端只需要检查Token、从Token中解析出客户端身份相关的数据即可,并不是必须在服务器端保存各Token数据,所以,Token可以设置较长时间的有效期,并不会长时间持续消耗服务器端的存储资源!所以,Token可以用于长时间表示用户的身份!

Token天生就适用于集群分布式系统,因为各服务器端只需要具有相同的验证并解析Token的程序,就可以识别客户端的身份。

其实,Token的传输流程与Session ID基本上是相同的,最大区别在于Session ID只是一个UUID数据,具有唯一性、随机性(不可预测性),但是,本身并不表示数据含义,而Token本身就是有数据含义的!

使用Jwt

JWT的官网https://jwt.io/

Jwt:(Json Web Token)

每个JWT数据都包含3个组成部分

关于JWT编程工具包https://jwt.io/libraries?language=Java

选取jwt工具包时,建议选取github获取星较多的,但其实任何jwt工具包都具备生成和解析的功能

可以选择在项目的pom.xml中添加依赖项:

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

生成并响应Jwt

在验证登录成功后,将之前存入SecurityConttext的操作弃用,改为返回生成的Jwt

AdminServiceImpl中编写生成Jwt的代码如下

@Slf4j
@Service
public class AdminServiceImpl implements IAdminService {

    @Value("${csmall.jwt.secret-key}")
    private String secretKey;

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

    @Autowired
    private AuthenticationManager authenticationManager;
        
    @Override
    //     ↓将返回值改为String类型
    public String login(AdminLoginDTO adminLoginDTO) {
        log.debug("开始处理验证登录的业务");

        Authentication authentication = new UsernamePasswordAuthenticationToken(
                adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
        );

        Authentication authenticationResult = authenticationManager.authenticate(authentication);
        log.debug("验证登录成功,验证返回的结果是:{}",authenticationResult);

        // -------------禁用此时存入SecurityContext的操作---------------------------  
//        log.debug("即将向SecurityContext中存入Authentication");
//        SecurityContext securityContext = SecurityContextHolder.getContext();
//        securityContext.setAuthentication(authenticationResult);

        
// 以下为新增   
        
        Map<String,Object> claims = new HashMap<>(); // 准备用户信息

        AdminDetails adminDetails = (AdminDetails) authenticationResult.getPrincipal();
        claims.put("id",adminDetails.getId());
        claims.put("username",adminDetails.getUsername());
        log.debug("jwt中的数据包含,{}",claims);

        Date exp = new Date(System.currentTimeMillis() + durationInMinute * 1000 * 60);

        String jwt = Jwts.builder()
                .setHeaderParam("alg", "HS256") // 此处设置生成jwt算法
                .setHeaderParam("typ", "JWT") // 此处表明生成的为jwt
                .setClaims(claims) // 此处为需要jwt中包含的数据
                .setExpiration(exp) // 设置jwt的有效期
                .signWith(SignatureAlgorithm.HS256, secretKey) // 指定验证签名
                .compact();
        log.debug("生成了JWT数据,并将返回此JWT数据:{}", jwt);
        return jwt;
    }
}

调整Controller中的代码,将返回的jwt数据响应到客户端去

@PostMapping("/login")
    public JsonResult login(AdminLoginDTO adminLoginDTO){
        log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
        String jwt = adminService.login(adminLoginDTO);
        return JsonResult.ok(jwt);// ←←←←←←←←
    }

客户端接受到的数据如下

{
  "state": 20000,
  "data": 																				"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZXhwIjoxNjgxNjI2ODQwLCJ1c2VybmFtZSI6InJvb3QifQ.9atdNiIRsGb6Ll4g58rLOBi5BoGQb1MoHFNsraCjwTo"
}

解析客户端携带的Jwt

客户端提交若干种不同的请求时,可能都需要携带JWT,在服务器端,处理若干种不同的请求之前也需要尝试接收解析JWT,则应该使用**过滤器(Filter)**组件进行处理

过滤器是Java服务器端的组件中,最早接收到请求的组件,它执行在其它任何组件之前!在同一个项目中,允许存在若干个过滤器,形成过滤器链(Filter Chian),任何一个请求,必须被所有过滤器“放行”才可以被后续的组件(例如Controller等)进行处理!

在项目的根包下创建filter.JwtAuthorizationFilter类,继承OncePerRequestFilter抽象类(将间接的实现Filter接口),并在类上添加组件注解

package cn.tedu.csmall.passport.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
    }

}

想要过滤器启用,还需要在Security配置类中将过滤器添加到过滤连中

添加效果如下

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    // 新增代码
    @Autowired
    private JwtAuthorizationFilter jwtAuthorizationFilter;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// 暂不关心其它代码
        
        // 将自定义的JWT过滤器添加在Spring Security的UsernamePasswordAuthenticationFilter之前
		http.addFilterBefore(jwtAuthorizationFilter, 
                     UsernamePasswordAuthenticationFilter.class);
    }
}

以上过滤器处理验证大抵可以分为三个步骤

​ `接收客户端传递过来的jwt

​ `解析jwt

​ `将解析完成的数据存入SecurityContext共框架后续使用

所以在重写的doFileterInternal方法中添加以下代码

/**
 * 此过滤器主要完成三件事情(要启用过滤器还需要在配置类中自动装配过滤器对象)
 * 1.通过header获取用户传递来的Jwt信息
 * 2.解析Jwt信息
 * 3.将解析后的数据存储到SecurityContext中供框架处理后面的过滤
 */
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    public static final int JWT_MIN_LENGTH = 113; // 携带的jwt最短长度

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 获取jwt信息
        String jwt = request.getHeader("Authorization");
        log.debug("接收用户携带的jwt :n{}",jwt);
        // 判断用户携带的jwt是否是有效的
        if(!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH){
            filterChain.doFilter(request,response);
            return ;
        }
        // 尝试解析Jwt
        String secretKey = "kU4jrFA3iuI5jn25u743kfDs7a8pFEwS54hm";
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();

        Long id = claims.get("id", Long.class);
        String username = claims.get("username", String.class);
        log.debug("从JWT中解析得到的管理员ID:{}", id);
        log.debug("从JWT中解析得到的管理员用户名:{}", username);

        // 将解析后的信息存储到SecurityContext中
        	//准备当事人信息
        LoginPrincipal loginPrincipal = new LoginPrincipal();
        loginPrincipal.setId(id);
        loginPrincipal.setUsername(username);
        Object principal = loginPrincipal;
            // 凭证,应该为null
        Object credentials = null;
        	// 权限列表
        Collection<SimpleGrantedAuthority> attributes = new ArrayList<>();
        attributes.add(new SimpleGrantedAuthority("假数据"));// 暂时不处理权限相关的问题
        
        // 将信息添加到Authentication对象中
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal,credentials,attributes);
        SecurityContext securityContext = SecurityContextHolder.getContext();
        // 最后存入SecurityContext中
        securityContext.setAuthentication(authentication);

        filterChain.doFilter(request,response);
    }
}

向SecurityContext中添加的Authentication对象需要三个参数第一个为当事人信息,第二个为凭证,第三个为权限列表

由于当事人信息需要基本包含用户id和用户名username,而Authentication中的Principal的类型是Object,所以,你可以使用任何类型的数据作为当事人,并且,在需要获取当事人信息时,添加@AuthenticationPrincipal注解参数也是你自行决定的当事人类型。

在项目的根包下创建security.LoginPrincipal类型,用于封装当事人信息:

@Data
public class LoginPrincipal implements Serializable {

    /**
     * 当事人ID
     */
    private Long id;
    /**
     * 当事人用户名
     */
    private String username;

}

此时已经基本能完成解析Jwt的操作,由于没有处理权限相关需要将Controller中的权限代码暂时禁用

// 测试使用
	  @GetMapping("/getPrincipal")
//    @PreAuthorize("hasAuthority('/ams/admin/read')")   暂时禁用
    public String getPrincipal(@AuthenticationPrincipal @ApiIgnore LoginPrincipal loginPrincipal) {
        log.debug("当事人用户id:{}",loginPrincipal.getId());
        log.debug("当事人用户名是:{}",loginPrincipal.getUsername());
        return "接收到【查询列表】请求,但未开发";
    }

如果登录成功应该返回

{
  "state": 20000,
  "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiZXhwIjoxNjgwOTM4MjU5LCJ1c2VybmFtZSI6ImxpdWNhbmdzb25nIn0.IfnaR3S5wBDyxqaQMRxcr_te8tDV3zjvzRSRY8Njghg"
}

将返回的jwt添加到api测试文档中的全局参数中,用于过滤器解析使用

[外链图片转存失败,源站可能防盗链机制,建议图片保存下来直接上传(img-MtmszR7o-1680855267814)(Spring%20Security.assets/image-20230407152132722.png)]

调用测试方法运行结果为:

​ [外链图片转存失败,源站可能防盗链机制,建议图片保存下来直接上传(img-d3JzRNQm-1680855267815)(Spring%20Security.assets/image-20230407152641651.png)]

就是controller中的测试方法输出内容

关于权限

当数据需要输出且后续还需要读取时,可能会涉及序列化问题,因为,当数据离开内存,就不再具有“数据类型”的含义了,包括将数据直接转换成字符串(例如将某数据存入到JWT中),则后续希望将字符还原成原本的类型时,可能是无法做到的!

业内用于解决序列化和反序列化问题常见手段就是使用JSON,先将对象转换成JSON格式的字符串,后续,需要还原时,再将JSON格式的字符串反序列化为对象。

可以使用阿里fastjson来解决此问题,添加依赖

<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

其实在之前的AdminServiceImpl.login()中的adminDetails中就含有权限信息

调整AdminServiceImpl中的代码,

[外链图片转存失败,源站可能有防盗链机制,建议图片保存下来直接上传(imggc78mz22-1680855267815)(Spring%20Security.assets/1680835416819.png)]

然后将过滤器中的权限相关代码修改

[外链图片转存失败,源站可能有防盗链机制,建议图片保存下来直接上传(img-1tUPAzgz-1680855267816)(Spring%20Security.assets/1680835561590.png)]

重新启用controller中的权限检查

// 测试使用
	  @GetMapping("/getPrincipal")
    @PreAuthorize("hasAuthority('/ams/admin/read')") // 启用
    public String getPrincipal(@AuthenticationPrincipal @ApiIgnore LoginPrincipal loginPrincipal) {
        log.debug("当事人用户id:{}",loginPrincipal.getId());
        log.debug("当事人用户名是:{}",loginPrincipal.getUsername());
        return "接收到【查询列表】请求,但未开发";
    }

完成后,可以重启项目,使用root管理员登录,可以执行所有操作,使用其他管理员部分操作是不允许的!(注意:更换登录的管理员后,在调试发送请求之前,需要更换为对应的JWT数据)

理解析JWT时的异常

由于解析JWT是在过滤器中执行的,而过滤器是整个服务器端中最早接收到任何请求的组件,此时,其它组件尚未开始处理当前请求,所以,不可以使用“全局异常处理器”来处理解析JWT时的异常全局异常处理器只能处理控制器抛出异常),则只能使用try...catch语法来处理异常

首先,在ServiceCode中补充新的业务状态码:

/**
 * 错误:JWT已过期
 */
ERR_JWT_EXPIRED(60000),
/**
 * 错误:验证签名失败
 */
ERR_JWT_SIGNATURE(60100),
/**
 * 错误:JWT格式错误
 */
ERR_JWT_MALFORMED(60200),

然后,调整JwtAuthorizationFilter中解析JWT的代码片段

// 客户端携带了基本有效的JWT,则尝试解析JWT
Claims claims = null;
response.setContentType("application/json; charset=utf-8;");
try {
    claims = Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(jwt)
            .getBody();
} catch (ExpiredJwtException e) {
    String message = "您的登录信息已过期,请重新登录!";
    log.warn(message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
} catch (SignatureException e) {
    String message = "非法访问!";
    log.warn(message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
} catch (MalformedJwtException e) {
    String message = "非法访问!";
    log.warn(message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
} catch (Throwable e) {
    String message = "服务器忙,请稍后再次尝试!(开发过程中,如果看到此提示,请检查控制台的信息,并在JWT过滤器补充处理此异常)";
    log.warn(message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
}

处理未登录的错误

当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源,则服务器端默认会响应403错误!

此问题需要在Spring Security的配置类中的configurer(HttpSecurity http)方法中添加配置来解决:

// 处理“当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源”的问题
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json; charset=utf-8;");
        String message = "未检测到登录信息,请登录!(在开发阶段看到此提示时,请检查客户端是否携带了有效的JWT数据)";
        log.warn(message);
        JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
        String jsonResultString = JSON.toJSONString(jsonResult);
        PrintWriter writer = response.getWriter();
        writer.println(jsonResultString);
        writer.close();
    }
});

sultString);
writer.close();
}


# 处理未登录的错误

当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源,则服务器端默认会响应`403`错误!

此问题需要在Spring Security的配置类中的`configurer(HttpSecurity http)`方法中添加配置来解决:

```java
// 处理“当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源”的问题
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json; charset=utf-8;");
        String message = "未检测到登录信息,请登录!(在开发阶段,看到此提示时,请检查客户端是否携带了有效的JWT数据)";
        log.warn(message);
        JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
        String jsonResultString = JSON.toJSONString(jsonResult);
        PrintWriter writer = response.getWriter();
        writer.println(jsonResultString);
        writer.close();
    }
});

以上就完成了全套的Spring Security的单点登录功能

原文地址:https://blog.csdn.net/weixin_72718065/article/details/130014998

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_42674.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注