Spring Security框架

关于Sp

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

认证信息:表示用户的身份的信息

认证:识别用户身份信息,具体可以表现为“登录”

授权:授予用户权限,使之可以进行某些访问,反之,如果用户没有得到相关授权,就不允许进行某些访问

Spring Security框架的依赖项

在Spring Boot项目中使用Spring Security时需要添加依赖项:
 

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

Spring Security的典型特征

当添加了spring-boot-starter-security依赖项,会自带一系列的自动配置,当启动项目后,相比此前的项目,会有以下变化:

  • 所有的请求(包括根本不存在的)都是必须要登录的,如果未登录,会自动跳转到框架自带的登录页面

  • 默认的用户名是user,密码是启用项目时在控制台提示的一串UUID值

    • 登录时,如果在打开登录页面后重启过服务器端,应该刷新登录页面,否则,第1次输入并提交是无效的。

    • 原因:当服务器重启后,由服务器生成的UUID值(该UUID值还应该保存在返回给客户端的登陆页面中)被清空了(Security本质还是Session),所以老页面中保存的UUID值已经失效了,当老登录页面带着UUID发来请求时,由于UUID失效,服务器会重新返回一个新的登录页面,新登录页面中保存者新的UUID值,新页面带着新的UUID发请求就没有问题了,具体表现是页面被刷新了一次,再一次发请求就可以成功了

  • 当登录成功后,会自动跳转到此前尝试访问的URL

  • 当登录成功后,可通过 /logout 退出登录

  • 默认不接受普通的POST请求,如果提交POST请求,会响应403(Forbidden)

    • 具体原因参见后续的CSRF相关内容
       

关于Spring Security的配置

 在项目的根包下创建SecurityConfiguration类,作为Spring Security的配置类,继承自WebSecurityConfigurerAdpater类,(高版本的Spring要求实现接口 WebSecurityConfigurer,继承的类也实现了该接口,归根结底还是要实现接口)并重写void configure(HttpSecurity http)方法,在方法体中进行配置:

@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http); // 不要保留调用父类同名方法的代码,不要保留!不要保留!不要保留!注释掉后所有请求不会再限制,即所有请求都允许通过,没有登录表单
    }
    
}

关于默认的登录表单

在配置类的void configure(HttpSecurity http)方法中,在没有调用父级的同名方法时,默认是不启用登录表单的!

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 如果调用以下方法,当需要访问通过认证的资源,但是未通过认证时,将自动跳转到登录页面
    // 如果未调用以下方法,将响应403
    http.formLogin(); // 添加后发 /login 请求可以访问登录页面,其他请求可以正常访问,列如Api在线文档访问正常

    // super.configure(http); // 不要保留调用父类同名方法的代码,不要保留!不要保留!不要保留!
}

关于请求的访问控制

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 白名单
    // 使用1个星号,表示通配此层级的任意资源,例如:/admins/*,可以匹配:/admins/add-new、/admins/delete
    // 但是,不可以匹配多个层级,例如:/admins/*,不可以匹配:/admins/9527/delete
    // 使用2个连续的星号,表示通配若干层级的任意资源,例如:/admins/*,可以匹配:/admins/add-new、/admins/9527/delete
    String[] urls = {
            "/doc.html",
            "/**/*.css",
            "/**/*.js",
            "/swagger-resources",
            "/v2/api-docs",
    };

    // 基于请求的访问控制
    http.authorizeRequests() // 对请求进行授权
        .mvcMatchers(urls) // 匹配某些路径
        .permitAll() // 直接许可,即不需要认证即可访问
        .anyRequest() // 任意请求
        .authenticated(); // 要求通过认证的
} // 添加该限制后请求不再像之前,除了白名单的请求外,其他请求均要登陆或者响应403(取决于是否写了http.formLogin())

使用临时的自定义的账号实现登录

在使用Spring Security框架时,可以自定义组件类,实现UserDetailsService接口,则Spring Security框架会基于此类的对象来处理认证!

在项目的根包下创建security.UserDetailsServiceImpl类,在类上添加@Service,实现UserDetailsService接口,重写接口中的方法:

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return null;
    }
   

使用数据库中的账号信息实现登录

然后,调整UserDetailsServiceImpl中的实现:
 

@Autowired
private AdminMapper adminMapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("xxx");
    AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
    if (loginInfo == null) {
        return null;
    }

    UserDetails userDetails = User.builder()
            .username(loginInfo.getUsername())
            .password(loginInfo.getPassword()) // 期望是密文
            .disabled(loginInfo.getEnable() == 0) // 账号禁用
            .accountLocked(false) // 账号锁定
            .accountExpired(false) // 账号过期
            .credentialsExpired(false) // 凭证过期
            .authorities("这是一个临时使用的山寨权限") // 权限
            .build();
    return userDetails;
}

由于数据库中的测试数据的密码都是密文的,例如:

$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C

 以上密文是通过BCrypt算法进行编码的结果!为了保证Spring Security能够正确的判断密码,需要将密码编码器改为BCrypt的密码编码器,例如:

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

关于Token

Token:令牌,票据

Token机制是用于解决服务器端识别客户端身份的。

关于JWT

在项目中添加JJWT的依赖项:

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

在项目中使用JWT识别用户的身份

  • 当用户通过登录的验证后,服务器应该生成JWT数据,并响应到客户端

    • 当通过验证后,不再需要(没有必要)将用户的认证信息存入到SecurityContext

  • 当用户尝试执行需要通过认证的操作时,用户应该自主携带JWT,并且,服务器端应该尝试解析此JWT,从而验证JWT的真伪,并识别用户的身份,如果一切无误,再将用户的认证信息存入到SecurityContext

登录成功后响应JWT

 调整AdminServiceImpl中的login()的声明与实现:

@Override
public String login(AdminLoginInfoDTO adminLoginInfoDTO) {
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginInfoDTO);
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            adminLoginInfoDTO.getUsername(), adminLoginInfoDTO.getPassword());
    Authentication authenticateResult
            = authenticationManager.authenticate(authentication);
    log.debug("认证通过!(如果未通过,过程中将抛出异常,你不会看到此条日志!)");
    log.debug("认证结果:{}", authenticateResult);
    log.debug("认证结果中的当事人:{}", authenticateResult.getPrincipal());

    // 使用JWT机制时,登录成功后不再需要将认证信息存入到SecurityContext
    // SecurityContext securityContext = SecurityContextHolder.getContext();
    // securityContext.setAuthentication(authenticateResult);
    // 需要存入到JWT中的数据
    AdminDetails adminDetails = (AdminDetails) authenticateResult.getPrincipal();
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", adminDetails.getId());
    claims.put("username", adminDetails.getUsername());
 // 生成JWT,以下代码是相对固定的
    String secretKey = "gfd89uiKa89J043tAFrflkji9432kjfdsajm";
    String jwt = Jwts.builder()
            // Header:声明算法与Token类型
            .setHeaderParam("alg", "HS256")
            .setHeaderParam("typ", "JWT")
            // Payload:数据,具体表现为Claims
            .setClaims(claims)
            .setExpiration(new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000))
            // Verify Signature:验证签名
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    log.debug("生成了JWT数据:{}", jwt);
    return jwt;
}

解析客户端携带的JWT

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

package cn.tedu.csmall.passport.filter;

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthorizationFilter extends OncePerRequestFilter {

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

    }
}

然后,在过滤器的方法中接收JWT数据:

@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("JWT过滤器开始执行……");
        // 根据业内惯用的做法,客户端提交的请求中的JWT应该存放于请求头(Request Header)中的名为Authorization属性中
        String jwt = request.getHeader("Authorization");
        log.debug("客户端携带的JWT:{}", jwt);

        // 放行
        filterChain.doFilter(request, response);
    }

}

然后,还需要在SecurityConfiguration中将此过滤器注册到Spring Security框架的过滤器链中:

 解析JWT

/**
 * <p>JWT过滤器</p>
 *
 * <p>此过滤器的主要作用</p>
 * <ul>
 *     <li>接收客户端提交的请求中的JWT</li>
 *     <li>尝试解析客户端提交的请求中的有效JWT</li>
 *     <li>将解析成功得到的数据创建为Authentication对象,并存入到SecurityContext中</li>
 * </ul>
 */
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    /**
     * JWT的最小长度值
     */
    public static final int JWT_MIN_LENGTH = 113;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("JWT过滤器开始执行……");
        // 根据业内惯用的做法,客户端提交的请求中的JWT应该存放于请求头(Request Header)中的名为Authorization属性中
        String jwt = request.getHeader("Authorization");
        log.debug("客户端携带的JWT:{}", jwt);

        // 判断客户端是否携带了有效的JWT
        if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
            // 如果JWT无效,直接放行
            log.debug("客户端没有携带有效的JWT,将放行,由后续的过滤器等组件继续处理此请求……");
            filterChain.doFilter(request, response);
            return;
        }

        // 尝试解析JWT
        String secretKey = "gfd89uiKa89J043tAFrflkji9432kjfdsajm";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Object id = claims.get("id");
        Object username = claims.get("username");
        log.debug("解析JWT结束,id={},username={}", id, username);

        // 临时处理认证信息中的权限
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("这是一个山寨的权限!"));

        // 创建Authentication对象
        Object principal = username;
        Object credentials = null;
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);

        // 将Authentication对象存入到SecurityContext中
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);

        // 放行
        filterChain.doFilter(request, response);
    }

}

关于当事人

 当事人信息中应该至少包含用户的ID和用户名,而认证信息(Authentication)中的当事人(Principal)的类型被设计为Object,所以,你可以使用任何类型的数据作为当事人!则可以自定义类封装用户的ID和用户名!

在解析JWT时,将解析结果处理为期望的类型,例如:

// 尝试解析JWT
String secretKey = "gfd89uiKa89J043tAFrflkji9432kjfdsajm";
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={},username={}", id, username);

然后,基于解析结果创建当事人对象:

// 创建当事人对象,用于存入到Authentication对象中
LoginPrincipal loginPrincipal = new LoginPrincipal();
loginPrincipal.setId(id);
loginPrincipal.setUsername(username);

然后,将当事人对象用于创建认证信息对象:

// 创建Authentication对象
Object principal = loginPrincipal; // 当事人对象
Object credentials = null;
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);

在控制器类中处理请求的方法中,当需要当事人数据时,注入LoginPrincipal类型的参数即可:

@GetMapping("")
public JsonResult<List<AdminListItemVO>> list(
        @ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {
    log.debug("开始处理【查询管理员列表】的请求,参数:无");
    log.debug("当事人信息:{}", loginPrincipal);
    log.debug("当事人信息中的ID:{}", loginPrincipal.getId());
    log.debug("当事人信息中的用户名:{}", loginPrincipal.getUsername());
    List<AdminListItemVO> list = adminService.list();
    return JsonResult.ok(list);
}

处理权限

在项目中添加fastjson依赖项:
 

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

 则任何管理员成功登录后,得到的JWT中都将包含权限列表的信息!

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值