Spring Security

一、Spring Security核心功能

  • Authentication:身份认证,用户登陆的验证(你是谁?)
  • Authorization:访问授权,授权资源的访问权限(你能干什么?)
  • 安全防护,防止跨站请求,session攻击等

二、工作原理

当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此 类,下图是Spring Security过虑器链结构图:

在这里插入图片描述

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时 这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理

在这里插入图片描述

  • SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext。
  • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变。
  • FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了。
  • ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出

三、认证流程

在这里插入图片描述

在这里插入图片描述

  • 1.用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  • 2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
  • 3.认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  • 4.SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过SecurityContextHolder.getContext().setAuthentication(…),设置到其中。可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。我们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。

四、PasswordEncoder密码加密

package org.springframework.security.crypto.password;
// 源码解读
public interface PasswordEncoder {
	// 接收原始密码的字符串,返回一个经过加密后的哈希值,不能逆向解密
    String encode(CharSequence var1);
	// 用来校验用户输入的密码,和加密后的密码比较,一般用于登录
    boolean matches(CharSequence var1, String var2);
	// 判断加密后的密码是否需要重新加密,默认是不需要。如果必须要一个月修改一次密码,可以重写这个方法
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

五、HttpBasic 认证模式

package com.zimug.courses.security.basic.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author pangjian
 * @ClassName SpringSecurityConfig
 * @Description Spring Security配置
 * @date 2021/6/3 9:56
 */
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * @Description: 这个方法的作用是进行安全认证及授权规则配置
     * @Param http:
     * @return void
     * @date 2021/6/3 10:03
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()// 开启httpbasic认证
            .and()
            .authorizeRequests()//资源控制逻辑
            .anyRequest()//所有请求
            .authenticated();//请求认证规则

    }
}

开启后访问系统资源就要求输入账号密码,密码是启动项目时就给你的,账号默认是user

在这里插入图片描述
在这里插入图片描述

自定义账号密码

在application.yml中配置

spring:
  security:
    user:
      name: admin
      password: admin

认证流程

在这里插入图片描述

破解方式

找到请求头的Authorization,复制它对应的值,去Base64解密出来即可


六、formLogin认证模式

  • formLogin登录认证不写Controller方法
  • 传统登录认证:请求->自己写controller验证用户名密码
  • formLogin登录认证,UsernamePasswordAuthenticationFilter
  • UsernamePasswordAuthenticationFilter过滤器是默认集成的,我们只需要针对它进行配置
<form action="/login" method="post">
    <span>用户名称</span><input type="text" name="name" /> <br>
    <span>用户密码</span><input type="password" name="psd" /> <br>
    <input type="submit" value="登陆">
</form>
package com.zimug.courses.security.basic.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author pangjian
 * @ClassName SpringSecurityConfig
 * @Description Spring Security配置
 * @date 2021/6/3 9:56
 */
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * @Description: 这个方法的作用是进行安全认证及授权规则配置
     * @Param http:
     * @return void
     * @date 2021/6/3 10:03
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()// 关闭跨站防御
            .formLogin() // 开启formLogin认证
                //登录认证逻辑
                .loginPage("/login.html") // 一旦用户请求没有权限就跳转到这个页面
                .loginProcessingUrl("/login")// 登录表单form中的action地址,也就是处理认证请求的路径
                .usernameParameter("name")// 接收表单数据
                .passwordParameter("psd")// 接收表单数据
                .defaultSuccessUrl("/")// 登录认证成功后默认跳转的路径
            .and()
                // 资源控制逻辑
                .authorizeRequests()
                .antMatchers("/login.html","/login")
                    .permitAll()// 允许任何人访问
                .antMatchers("/","biz1","biz2")
                    .hasAnyAuthority("ROLE_user","ROLE_admin")//给上面的资源加上权限规则
                .antMatchers("/syslog","/sysuser")
                    .hasAnyRole("admin")//给上面资源加上权限规则
            .anyRequest()
            .authenticated();// 所有请求都需要登录认证才能访问
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                // 静态写死该用户的用户名user和密码123456,还给他固定的角色user,然后该用户只能访问biz1.html和biz2.html
                .withUser("user")
                .password(passwordEncoder().encode("123456"))
                .roles("user")
            .and()
                .withUser("admin")
                .password(passwordEncoder().encode("123456"))
                .roles("admin")
            .and()
                .passwordEncoder(passwordEncoder());// 配置BCrypt加密

    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 将项目中的静态资源路径开发,这里配置是不需要经过Filter过滤器的
        web.ignoring().antMatchers("/css/**","/fonts/**","/img/**","/js/**");
    }
}

七、解读核心组件

UsernamePasswordAuthenticationFilter类

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
	// 只允许post协议进行认证
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
    	// 获取用户名和密码
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        // 把用户名和密码构建成一个token
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        // 调用方法对这个token进行认证,认证成功返回一个Authentication登录认证的主体
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

UsernamePasswordAuthenticationFilter是Authtication的子类,认证之前是token令牌,认证之后是主体信息

它的父类AbstractAuthenticationProcessingFilter有两个属性

private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

登录成功或失败后的处理器,如果我们登录失败了不想跳转其他页面可用实现它的接口方法

public interface AuthenticationFailureHandler {
    void onAuthenticationFailure(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3) throws IOException, ServletException;
}

AuthenticationManager接口

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationManager {
	// 只有一个登录认证的方法,要传入一个登录认证主体,也就是前端传过来的信息被封装成了一个Authentication
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

也就是如下Authentication接口,看看它有哪些属性

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {
	// 返回一个权限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

	// 用户细节
    Object getDetails();

	// 用户主体信息
    Object getPrincipal();

	// 主体是否被认证,如果没有认证是不能访问后端的接口的
    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

AuthenticationProvider

ProviderManager是AuthenticationManager的实现核心类,用来登录认证的,ProviderManager有这一个属性:

private List<AuthenticationProvider> providers;

DaoAuthenticationProvider是AuthenticationProvider的实现类,用来去数据库加载用户名、密码和权限的类,它有下面这个核心的方法:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
        	// 重要,它根据用户名username去加载该用户的信息,用一个UserDetails去接收信息
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

UserDetails

UserDetails接口有什么属性,这个信息要从数据库查询出来

public interface UserDetails extends Serializable {
	// 登录用户的权限
    Collection<? extends GrantedAuthority> getAuthorities();
	// 登录用户的密码
    String getPassword();
	// 用户名
    String getUsername();
	// 账户是否过期
    boolean isAccountNonExpired();
	// 账户是否被锁定
    boolean isAccountNonLocked();
	
    boolean isCredentialsNonExpired();
	// 当前账户是否可用
    boolean isEnabled();
}

返回认证消息

查询出来的主体信息(Authentication)会保存到SecurityContext,也就是上下文中,它已经经过认证了,下次登录就直接从上下文拿出来,不需要经过那么多过滤了,直接访问Controller

八、自定义验证成功或失败后的跳转逻辑

认证失败

package com.zimug.courses.security.basic.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zimug.courses.security.basic.Resp.Resp;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 * @author pangjian
 * @ClassName MyAuthenticationFailureHandler
 * @Description 自定义验证失败后跳转逻辑,返回和接收的要是json数据
 * @date 2021/6/3 12:56
 */
@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {


    @Value("${spring.security.logintype}")
    private String loginType;

    // 将对象变成json数据的处理对象
    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        if(loginType.equalsIgnoreCase("JSON")){
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(
                    Resp.fail()
            ));
        } else {
            response.setContentType("application/json;charset=UTF-8");
            super.onAuthenticationFailure(request,response,exception);
        }
    }
}

认证成功

package com.zimug.courses.security.basic.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zimug.courses.security.basic.Resp.Resp;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
 * @author pangjian
 * @ClassName MyAuthenticationSuccessHandler
 * @Description 自定义验证成功后跳转逻辑
 * @date 2021/6/3 12:49
 */
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Value("${spring.security.logintype}")
    private String loginType;

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        if(loginType.equalsIgnoreCase("JSON")){
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(
                    Resp.success())
            );
        } else {
            // 会帮我们跳转到上次请求的页面上
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }
}

注入自定义设置

package com.zimug.courses.security.basic.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

/**
 * @author pangjian
 * @ClassName SpringSecurityConfig
 * @Description Spring Security配置
 * @date 2021/6/3 9:56
 */
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    /**
     * @Description: 这个方法的作用是进行安全认证及授权规则配置
     * @Param http:
     * @return void
     * @date 2021/6/3 10:03
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()// 关闭跨站防御
            .formLogin() // 开启formLogin认证
                //登录认证逻辑
                .loginPage("/login.html") // 一旦用户请求没有权限就跳转到这个页面
                .loginProcessingUrl("/login")// 登录表单form中的action地址,也就是处理认证请求的路径
                .usernameParameter("name")// 接收表单数据
                .passwordParameter("psd")// 接收表单数据
                // .defaultSuccessUrl("/")// 登录认证成功后默认跳转的路径
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
    }
}

九、动态加载用户角色权限

  • UserDetails接口表达你是谁?你有什么角色权限?

这个接口的信息要提供给spring security才能进行认证鉴权

  • UserDetailsService接口表达的是如何动态加载UserDetails数据

通过UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException方法去数据库加载

1.实现UserDetails接口

package com.zimug.courses.security.basic.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @author pangjian
 * @ClassName User
 * @Description SpringSecurity通过实现的接口方法去获取该实例化的值,我们通过set()方法注入后,在UserDetailsService接口就能返回该User实现类的实例化对象,让Spring Security拿到该自定义用户的信息
 * @date 2021/6/3 21:09
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {

    private String password;
    private String username;
    private boolean enabled;
    private Collection<? extends GrantedAuthority> authorities;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    // 我们不希望那么麻烦去限制用户的登录,只通过enabled就行,就给其他的返回一个默认值true给他通过
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

2.实现UserDetailsService接口去数据库动态加载数据

@Override
@Bean
public UserDetailsService userDetailsService(){
    return username-> {
        Admin admin = adminService.getAdminByUserName(username);
        if(null!=admin){
            List<String> roleList = roleMapper.selectRoleByUsername(admin.getUsername());

            List<String> authorities = menuMapper.selectMenuByRole(admin.getUsername());

            authorities.addAll(roleList);

            admin.setAuthorities(
                    AuthorityUtils.commaSeparatedStringToAuthorityList(
                            String.join(",",authorities)
                    )
            );
            // 该自定义的用户类已经实现了UserDetails接口,刚好符合了该方法的返回类型
            return admin;
        }
        return null;
    };
}

十、Spring Security整合JWT

JWT认证鉴权流程

在这里插入图片描述

JWT整合spring security的认证流程

在这里插入图片描述

JWT整合spring security的鉴权流程

在这里插入图片描述

代码实现

一、编写token工具类

package cn.guet.server.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

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

/**
 * @author pangjian
 * @ClassName JwtTokenUtil
 * @Description Jwt工具类
 * @date 2021/6/2 19:37
 */
@Component
public class JwtTokenUtil {

    private static final String CLAIM_KEY_USERNAME="sub";
    private static final String CLAIM_KEY_CREATED="created";

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;


    /**
     * @Description: 根据已成功登录的用户信息生成token
     * @Param userDetails:spring security框架中的接口
     * @return java.lang.String
     * @date 2021/6/2 19:46
    */
    public String generateToken(UserDetails userDetails){
        Map<String,Object> claim = new HashMap<>();
        claim.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claim.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claim);
    }

    /**
     * @Description: 验证token,一要认证从 前端传过来的token的里面的用户名 和 账号密码查询出来的用户名 是否一致,二要认证时间是否过期
     * @Param token: 待认证的token
     * @Param userDetails: spring security框架中的类
     * @return boolean
     * @date 2021/6/2 20:04
    */
    public boolean validateToken(String token,UserDetails userDetails){
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
    
    /**
     * @Description: token是否能被刷新,没过期是不可以被刷新,过期了可以被刷新
     * @Param token: 
     * @return boolean
     * @date 2021/6/2 20:17
    */
    public boolean canRefresh(String token){
        return !isTokenExpired(token);
    }

    /**
     * @Description: 刷新token
     * @Param token:
     * @return java.lang.String
     * @date 2021/6/2 20:21
    */
    public String refreshToken(String token){
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claims);
    }


    /**
     * @Description: 判断token是否失效
     * @Param token:
     * @return boolean
     * @date 2021/6/2 20:05
    */
    private boolean isTokenExpired(String token) {
        Date expireDate = getExpiredDateFromToken(token);
        // 过期时间要在当前时间之后
        return expireDate.before(new Date());
    }

    /**
     * @Description: 从token中获取过期时间
     * @Param token:
     * @return java.util.Date
     * @date 2021/6/2 20:09
    */
    private Date getExpiredDateFromToken(String token) {

        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();

    }

    /**
     * @Description: 从token中获取登录用户名
     * @Param token: token
     * @return java.lang.String
     * @date 2021/6/2 19:53
    */
    public String getUserNameFromToken(String token){
        String username;
        try{
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * @Description:
     * @Param token: 从token中获取荷载,需要密钥
     * @return io.jsonwebtoken.Claims
     * @date 2021/6/2 19:57
    */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
               claims = Jwts.parser()
                            .setSigningKey(secret)
                            .parseClaimsJws(token)
                            .getBody();
        } catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }


    /**
     * @Description: 根据荷载生成token
     * @Param claim: 荷载
     * @return java.lang.String
     * @date 2021/6/2 19:51
    */
    private String generateToken(Map<String,Object> claim){
        return Jwts.builder()
                .setClaims(claim)
                // 设置过期时间:当前时间加失效时间
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * @Description: 生成失效时间
     * @return java.util.Date
     * @date 2021/6/2 19:49
    */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis()+expiration*1000);
    }

}

二、实现UserDetails

这个接口的信息要提供给spring security才能进行认证鉴权
package cn.guet.server.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

/**
 * <p>
 *  通过实现的接口方法去获取该实例化的值,我们通过set()方法注入后,在UserDetailsService接口就能返回该User实现类的实例化对象,让Spring Security拿到该自定义用户的信息
 * </p>
 *
 * @author pangjian
 * @since 2021-06-02
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_admin")
@ApiModel(value="Admin对象", description="")
public class Admin implements Serializable , UserDetails {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "姓名")
    private String name;

    @ApiModelProperty(value = "手机号码")
    private String phone;

    @ApiModelProperty(value = "住宅电话")
    private String telephone;

    @ApiModelProperty(value = "联系地址")
    private String address;

    @ApiModelProperty(value = "是否启用")
    private Boolean enabled;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "用户头像")
    private String userFace;

    @ApiModelProperty(value = "备注")
    private String remark;

    @TableField(exist = false)
    private List<GrantedAuthority> authorities;

    /**
     * @Description:用户权限
     * @return java.util.Collection<? extends org.springframework.security.core.GrantedAuthority>
     * @date 2021/8/5 10:46
    */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * @Description:用不到的判断字段统一返回true
     * @return boolean
     * @date 2021/8/5 10:44
    */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * @Description:我们用到了账号是否被锁定,返回从数据库查出来的真实数据
     * @return boolean
     * @date 2021/8/5 10:45
    */
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

三、自定义Spring Security配置类

自己写一个类继承WebSecurityConfigurerAdapter,然后重写它的三个方法

package cn.guet.server.config;

import cn.guet.server.Filter.JwtFilter;
import cn.guet.server.mapper.MenuMapper;
import cn.guet.server.mapper.RoleMapper;
import cn.guet.server.pojo.Admin;
import cn.guet.server.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.List;

/**
 * @author pangjian
 * @ClassName SercutiyConfig
 * @Description Spring Security 配置
 * @date 2021/6/2 22:18
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AdminService adminService;

    @Autowired
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;

    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;


    /**
     * @Description: 用自定义的UserDetailsService(自定义去数据库加载数据),PasswordEncoder(密码加密)的bean
     * @Param auth:
     * @return void
     * @date 2021/8/5 10:29
    */
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{

        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    /**
     * @Description: 可以跳过过滤器选中放行一部分接口
     * @Param web:
     * @return void
     * @date 2021/6/14 20:38
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 放行一些访问路径
        web.ignoring().antMatchers(
                "/css/**",
                "/js/**",
                "/index.html",
                "/favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**"
        );
    }

    /**
     * @Description:配置拦截规则
     * @Param httpSecurity:
     * @return void
     * @date 2021/6/14 20:39
    */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{
        // 使用了JWT,先关掉csrf
        httpSecurity.csrf()
                        .disable()
                // 基于token,不需要session
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                    .antMatchers("/login","/logout")
                        .permitAll()
                    // 剩下所有请求都要求认证
                    .anyRequest()
                        .authenticated()
                .and()
                    // 禁用缓存
                    .headers()
                    .cacheControl();

        // 添加jwt 登录授权过滤器,在UsernamePasswordAuthenticationFilter前面执行
        httpSecurity.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
        // 添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);

    }


    /**
     * @Description:重写了loadUserByUsername方法,去数据库加载用户信息和用户权限,构建一个UserDetails接口实现类返回
     * @return org.springframework.security.core.userdetails.UserDetailsService
     * @date 2021/6/14 15:58
    */
    @Override
    @Bean
    public UserDetailsService userDetailsService(){
        return username-> {
            Admin admin = adminService.getAdminByUserName(username);
            if(null!=admin){
                List<String> roleList = roleMapper.selectRoleByUsername(admin.getUsername());

                List<String> authorities = menuMapper.selectMenuByRole(admin.getUsername());

                authorities.addAll(roleList);

                admin.setAuthorities(
                        AuthorityUtils.commaSeparatedStringToAuthorityList(
                                String.join(",",authorities)
                        )
                );
                // 该自定义的用户类已经实现了UserDetails接口,刚好符合了该方法的返回类型
                return admin;
            }
            return null;
        };
    }

    /**
     * @Description:密码加密
     * @return org.springframework.security.crypto.password.PasswordEncoder
     * @date 2021/8/5 10:27
    */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * @Description:前置token过滤器
     * @return cn.guet.server.Filter.JwtFilter
     * @date 2021/8/5 10:27
    */
    @Bean
    public JwtFilter jwtFilter(){
        return new JwtFilter();
    }

}

四、捕获异常,自定义返回友好结果提示

1.没有权限异常返回结果

package cn.guet.server.config;

import cn.guet.server.RespObject.RespBean;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * @author pangjian
 * @ClassName RestfulAccessDeniedHandler
 * @Description 当访问接口没有权限时,自定义返回结果
 * @date 2021/6/14 16:15
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        PrintWriter writer = httpServletResponse.getWriter();
        RespBean bean = RespBean.error("权限不足,请联系管理员");
        bean.setCode(403);
        writer.write(new ObjectMapper().writeValueAsString(bean));
        writer.flush();
        writer.close();

    }
}

2.没有登录或者token失效异常返回结果

package cn.guet.server.config;

import cn.guet.server.RespObject.RespBean;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * @author pangjian
 * @ClassName RestAuthorizetionEntryPoint
 * @Description 当未登录或者token失效时访问接口时,自定义返回结果
 * @date 2021/6/14 16:08
 */
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        PrintWriter printWriter = httpServletResponse.getWriter();
        RespBean respBean = RespBean.error("尚未登录,请登录");
        respBean.setCode(401);
        printWriter.write(new ObjectMapper().writeValueAsString(respBean));
        printWriter.flush();
        printWriter.close();
    }
}

五、编写前置token拦截器

目的是为了拦截token,并对它进行认证,确保当前请求用户已经登录和有相关权限访问此次要访问控制器方法

package cn.guet.server.Filter;

import cn.guet.server.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;

/**
 * @author pangjian
 * @ClassName JwtFilter
 * @Description JWT 登录授权过滤器
 * @date 2021/6/2 22:33
 */

public class JwtFilter extends OncePerRequestFilter {


    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 先根据key,是否能拿到value
        String authHeader = httpServletRequest.getHeader(tokenHeader);

        // 判断登录用户的token不为空和是Bearer开头的
        if(null!=authHeader && authHeader.startsWith(tokenHead)){
            // 取到token
            String authToken = authHeader.substring(tokenHead.length());
            // 从用户请求携带的token获取用户名,能取到证明token除了时间以外都合法了
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            // token 存在用户名但没有认证的
            if(null != username && null == SecurityContextHolder.getContext().getAuthentication()){
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                // 根据userDetails验证了token是否有效(验证时间是否过期和当前用户名是否匹配)
                if(jwtTokenUtil.validateToken(authToken,userDetails)){
                    // 我们的token,框架是不认识的,token有效就转化构建UsernamePasswordAuthenticationToken表示认证通过和进行相关授权
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    // 设置了认证主体,到UsernamePasswordAuthenticationFilter就不会拦截,因为你应该带有了它的token
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        // 继续执行其他过滤器
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值