栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > Java

Spring Security详解(1)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring Security详解(1)

文章目录
  • 前言
  • 一、准备工作
  • 二、关于JWT
    • session
    • Token
  • 三、关于JWT在项目中的应用
    • 生成JWT
    • 关于业务实现类的登录方法:
  • 四、 在服务器端检查并解析JWT
  • 总结


前言

提示:这里可以添加本文要记录的大概内容:
之前介绍了Spring Security是主要解决认证(Authenticate)和授权(Authorization)的框架。详细见关于Spring Security
下面主要介绍如何使用Security开发自定义的登录流程


提示:以下是本篇文章正文内容,下面案例可供参考

一、准备工作

此前,登录是由Security框架提供的页面的表单来输入用户名、密码,且由Security框架自动处理登录流程,不适合前后端分离的开发模式!所以,需要自行开发登录流程!

关于自定义的登录流程,主要需要:

  • 在业务逻辑实现类中,调用Security的验证机制来执行登录认证
  • 在控制器类中,自定义处理请求,用于接收登录请求及请求参数,并调用业务逻辑实现类实现认证

关于在Service中调用Security的认证机制:

当需要调用Security框架的认证机制时,需要使用AuthenticationManager对象,可以在Security配置类中重写authenticationManager()方法,在此方法上添加@Bean注解,由于当前类本身是配置类,所以Spring框架会自动调用此方法,并将返回的结果保存到Spring容器中:

@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}

在IAdminService中添加处理登录的抽象方法:

    
    String login(AdminLoginDTO adminLoginDTO);

在AdminServiceImpl中,可以自动装配AuthenticationManager对象,并实现接口中的方法:

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void login(AdminLoginDTO adminLoginDTO) {
    // 日志
    log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
    // 调用AuthenticationManager执行认证,创建认证对象,即传入用户名和密码
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
     //走Spring Security的认证流程,即loadUserByUsername(String s)
    authenticationManager.authenticate(authentication);
    log.debug("认证通过!");

在根包下创建pojo.dto.AdminLoginDTO类:

@Data
public class AdminLoginDTO implements Serializable {
    private String username;
    private String password;
}

在AdminController中添加处理请求的方法:

@ApiOperation("管理员登录")
@ApiOperationSupport(order = 50)
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO) {
    log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO);
    adminService.login(adminLoginDTO);
    return JsonResult.ok();
}

为了保证能对以上路径直接发起请求,需要将此路径(/admins/login)添加到Security配置类的“白名单”中。

完成后,启动项目,可以通过Knife4j的调试来测试登录,当登录成功时将响应正确,当用户名或密码错误时,将响应错误(需要统一处理异常)。

**注意:即使登录成功,也不可以实现其它请求的访问

二、关于JWT session

HTTP协议本身是无状态协议,所以,无法识别用户的身份!

为了解决此问题,经编程时,引入了Session机制,用于保存用户的某些信息,可识别用户的身份!

Session的本身是在服务器端的内存中一个类似Map结构的数据,每个客户端在提交请求时,都会携带一个由服务器端首次响应时分配的Session ID,作为Map的Key,由于此Session ID具有极强的唯一性,所以,每个客户端的Session ID理论上都是不相同的,从而服务器可以识别客户端!

由于Session是保存在服务器端的内存中的,在一般使用时,并不适用于集群!

Token

Token:令牌,票据。

目前,推荐使用Token来保存用户的身份标识,使之可以用于集群!

相比Session ID是没有信息含义的,Token则是有信息含义的数据,当客户端向服务器端提交登录请求后,服务器商认证通过就会将此用户的信息保存在Token中,并将此Token响应到客户端,后续,客户端在每次请求时携带Token,服务器端即可识别用户的身份!

JWT = JSON Web Token

JWT是使用JSON格式表示一系列的数据的Token。

当需要使用JWT时,应该在项目中添加依赖:



    io.jsonwebtoken
    jjwt
    0.9.1

然后,通过测试,实现生成JWT和解析JWT

@Slf4j
public class JwtTests {
    // 密钥(盐),用于生成jwt和解析jwt
    String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
    // 测试生成JWT
    @Test
    public void testGenerateJwt() {
        // 准备Claims
        Map claims = new HashMap<>();
        claims.put("id", 9527);
        claims.put("name", "liulaoshi");
        
        // JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
        String jwt = Jwts.builder()
                // Header:用于声明算法与此数据的类型,以下配置的属性名是固定的
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "jwt")
                // Payload:用于添加自定义数据,并声明有效期
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 3 * 60 * 1000))
                // Signature:用于指定算法与密钥(盐)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        log.debug("JWT = {}", jwt);
        // eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTUzMH0.lwD_PzrqGXEgQs3KmMjsYzTmhsKbGhKnd1WkDkFpj5M
    }

    @Test
    public void testParseJwt() {
    	//解析JWT
        String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTUzMH0.lwD_PzrqGXEgQs3KmMjsYzTmhsKbGhKnd1WkDkFpj5M";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Object id = claims.get("id");
        Object name = claims.get("name");
        log.debug("id={}", id);
        log.debug("name={}", name);
    }
}
三、关于JWT在项目中的应用 生成JWT

应该在用户登录时,视为”认证成功“后,生成JWT,并将此数据响应到客户端。

在业务层,调用AuthenticationManager的authenticate()方法后,得到的返回结果例如:

UsernamePasswordAuthenticationToken [
	Principal=org.springframework.security.core.userdetails.User [
		Username=root, 
		Password=[PROTECTED], 
		Enabled=true, 
		AccountNonExpired=true, 
		credentialsNonExpired=true, 
		AccountNonLocked=true, 
		Granted Authorities=[权限列表...]
	], 
	Credentials=[PROTECTED], 
	Authenticated=true, 
	Details=null, 
	Granted Authorities=[权限列表...]
]

可以看到,认证返回的数据中将包含成功认证的用户信息,也是当初用于执行认证的信息(UserDetailsServiceImpl中返回的结果),可以从此认证结果中获取用户相关数据,并写入到JWT中,则需要:

  • 将业务接口中的登录方法返回值类型改为String,表示认证成功后返回的JWT
  • 将业务实现类中的登录方法返回值一并修改
  • 在业务实现类中,当认证成功后,获取需要写入到JWT中的数据(例如:用户名等),并生成JWT,返回JWT
关于业务实现类的登录方法:
@Override
public String login(AdminLoginDTO adminLoginDTO) {
    // 日志
    log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
    // 调用AuthenticationManager执行认证
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
    Authentication authenticateResult = authenticationManager.authenticate(authentication);
    log.debug("认证通过,返回的结果:{}", authenticateResult);
    log.debug("认证结果中的Principal的类型:{}",
            authenticateResult.getPrincipal().getClass().getName()); //org.springframework.security.core.userdetails.User类型

    // 处理认证结果
    User loginUser = (User) authenticateResult.getPrincipal();
    log.debug("认证结果中的用户名:{}", loginUser.getUsername());

    // 生成JWT
    String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
    // 准备Claims
    Map claims = new HashMap<>();
    claims.put("username", loginUser.getUsername());
    // JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
    String jwt = Jwts.builder()
            // Header:用于声明算法与此数据的类型,以下配置的属性名是固定的
            .setHeaderParam("alg", "HS256")
            .setHeaderParam("typ", "jwt")
            // Payload:用于添加自定义数据,并声明有效期
            .setClaims(claims)
            .setExpiration(new Date(System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000))
            // Signature:用于指定算法与密钥(盐)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    log.debug("生成的JWT:{}", jwt);
    return jwt;
}

在控制器中,将处理登录请求的方法的返回值类型改为JsonResult,并在调用业务方法时获取返回值,封装到返回的对象中:

@ApiOperation("管理员登录")
@ApiOperationSupport(order = 50)
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO) {
    log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO);
    String jwt = adminService.login(adminLoginDTO);
    return JsonResult.ok(jwt);
}

完成后,重启项目,在Knife4j的调试功能中,使用正常的用户名和密码发起登录请求,将响应JWT结果,例如:

{
  "state": 20000,
  "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJleHAiOjE2NjExNTIzOTUsInVzZXJuYW1lIjoic3VwZXJfYWRtaW4ifQ.rFACBsBY8w8oNpR80n2YiplsEUIqw5bnCIsC5UAqsww"
}
四、 在服务器端检查并解析JWT

经过以上登录认证并响应JWT后,客户端在后续发起请求时,应该自主携带JWT数据,而服务器端应该尝试检查并解析JWT。

由于客户端在发起多种不同请求时都应该携带JWT,且服务器端都应该检查并尝试解析,所以,服务器端检查并解析的过程,应该发生在比较”通用“的组件中,即无论客户端提交的是哪个路径的请求,这个组件都应该执行!通常,会使用过滤器组件进行处理。

在项目的根包下创建filter.JwtAuthrozationFilter类,继承自OncePerRequestFilter,并在此类上添加@Component注解:

@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    public JwtAuthorizationFilter() {
        log.debug("创建过滤器:JwtAuthorizationFilter");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("执行JwtAuthorizationFilter");

        // 过滤器链继续执行,相当于:放行
        filterChain.doFilter(request, response);
    }

}

关于客户端提交请求时携带JWT数据,业内通用的做法是在请求头中添加Authorization属性,其值就是JWT数据,所以,服务器端获取JWT的做法是:从请求头中的Authorization属性中获取JWT数据!

@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    public JwtAuthorizationFilter() {
        log.debug("创建过滤器:JwtAuthorizationFilter");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        log.debug("执行JwtAuthorizationFilter");
        // 从请求头中获取JWT
        String jwt = request.getHeader("Authorization");
        log.debug("从请求头中获取JWT:{}", jwt);

        // 判断JWT数据是否不存在
        if (!StringUtils.hasText(jwt) || jwt.length() < 80) {
            log.debug("获取到的JWT是无效的,直接放行,交由后续的组件继续处理!");
            // 过滤器链继续执行,相当于:放行
            filterChain.doFilter(request, response);
            // 返回,终止当前方法本次执行
            return;
        }

        // 尝试解析JWT
        String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Object username = claims.get("username");
        log.debug("从JWT中解析得到username:{}", username);

        // 准备Authentication对象,后续会将此对象封装到Security的上下文中
        List authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("临时使用的权限"));
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                username, null, authorities);

        // 将用户信息封装到Security的上下文中
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);
        log.debug("已经向Security的上下文中写入:{}", authentication);

        // 过滤器链继续执行,相当于:放行
        filterChain.doFilter(request, response);
    }
}

完成后,还需要将此过滤器添加在Security框架的UsernamePasswordAuthenticationFilter过滤器之前,需要在Security配置类中,先自动装配自定义的过滤器对象:

@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;

然后,在configurer(HttpSecurity http)方法中添加:

// 将“JWT过滤器”添加在“认证过滤器”之前
http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);

最后,在JWT过滤器执行之初,先清除Security上下文中的数据,以避免”一旦提交JWT将认证对象存入到Security上下文中,后续不携带JWT也能访问“的问题:

// 清除Security上下文中的数据
SecurityContextHolder.clearContext();

完成后,启动项目,在Knife4j的调试功能中,携带JWT可以发起任何需要登录才能访问的请求,反之,这些请求不携带JWT将不允许访问。

总结

提示:这里对文章进行总结:

以上就是今天要讲的内容,本文介绍了如何使用Security开发自定义的登录流程,以及如何将JWT在项目中的应用。简而言之就是在用户登录时,视为”认证成功“后,生成JWT,并将此数据响应到客户端。经过以上登录认证并响应JWT后,客户端在后续发起请求时,应该自主携带JWT数据,而服务器端应该尝试检查并解析JWT。

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1039905.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号