1

创建项目

生成项目模板

为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站。

1.  打开浏览器,访问:https://start.spring.io/

2.  根据页面提示,选择构建工具,开发语言,项目信息等。
  生成项目模板
3. 点击 Generate,生成项目模板,生成之后会将压缩包下载到本地。
4. 解压demo.zip文件,然后idea导入pom.xml文件,项目结构如下:
项目结构

添加相关依赖

添加 Maven 相关依赖,这里需要添加上web、swagger、spring security、jwt和fastjson的依赖,Swagge和fastjson的添加是为了方便接口测试。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent> <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>2.4.0</version>
 <relativePath/> <!-- lookup parent from repository -->
 </parent>
 <groupId>com.spring.security</groupId>
 <artifactId>demo</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>demo</name>
 <description>Demo project for Spring Boot</description>
 <properties> <java.version>1.8</java.version>
 </properties>
 <dependencies> <!-- web -->
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency> <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
 </dependency>
 <!-- swagger -->
 <dependency>
 <groupId>io.springfox</groupId>
 <artifactId>springfox-swagger2</artifactId>
 <version>2.9.2</version>
 </dependency> <dependency> <groupId>io.springfox</groupId>
 <artifactId>springfox-swagger-ui</artifactId>
 <version>2.9.2</version>
 </dependency> <!-- spring security -->
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency> <!-- jwt -->
 <dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt</artifactId>
 <version>0.9.1</version>
 </dependency> <!-- fastjson -->
 <dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>fastjson</artifactId>
 <version>1.2.58</version>
 </dependency> <!-- 集成lombok 框架 -->
 <dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <optional>true</optional>
 </dependency> </dependencies>
 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 </plugin> </plugins> <!-- 打包时拷贝MyBatis的映射文件 -->
 <resources>
 <resource> <directory>src/main/java</directory>
 <includes> <include>**/sqlmap/*.xml</include>
 </includes> <filtering>false</filtering>
 </resource> <resource> <directory>src/main/resources</directory>
 <includes> <include>**/*.*</include>
 </includes> <filtering>true</filtering>
 </resource> </resources> </build>
</project>

添加相关配置

  • 添加swagger 配置

添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类,除了常规配置外,加了一个令牌属性,可以在接口调用的时候传递令牌。
SwaggerConfig.java

package com.spring.security.demo.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
 * @Author wulongbo
 * @Date 2020/11/21 11:50
 * @Version 1.0
 */@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
 public Docket createRestApi(){
        // 添加请求参数,我们这里把token作为请求头部参数传入后端
 ParameterBuilder parameterBuilder = new ParameterBuilder();
 List<Parameter> parameters = new ArrayList<Parameter>();
 parameterBuilder.name("Authorization").description("令牌").modelRef(new ModelRef("string")).parameterType("header")
                .required(false).build();
 parameters.add(parameterBuilder.build());
 return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any()).build().globalOperationParameters(parameters);
 }
    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("SpringBoot API Doc")
                .description("This is a restful api document of Spring Boot.")
                .version("1.0")
                .build();
 }
}
  • 添加跨域 配置

添加一个CORS跨域配置类,在工程下新建 config 包并添加一个 CorsConfig配置类。
CorsConfig.java

package com.spring.security.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * @Author wulongbo
 * @Date 2020/11/21 11:52
 * @Version 1.0
 */@Configuration
public class CorsConfig implements WebMvcConfigurer {
        @Override
 public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")    // 允许跨域访问的路径
 .allowedOrigins("*")    // 允许跨域访问的源
 .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")    // 允许请求方法
 .maxAge(168000)    // 预检间隔时间
 .allowedHeaders("*")  // 允许头部设置
 .allowCredentials(true); // 是否发送cookie
 }
}
  • 安全配置类

下面这个配置类是Spring Security的关键配置。

在这个配置类中,我们主要做了以下几个配置:

  1. 访问路径URL的授权策略,如登录、Swagger访问免登录认证等
  2. 指定了登录认证流程过滤器 JwtLoginFilter,由它来触发登录认证
  3. 指定了自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService
  4. 指定了访问控制过滤器 JwtAuthenticationFilter,在授权时解析令牌和设置登录状态
  5. 指定了退出登录处理器,因为是前后端分离,防止内置的登录处理器在后台进行跳转

WebSecurityConfig.java

package com.spring.security.demo.config;
import com.spring.security.demo.security.JwtAuthenticationFilter;
import com.spring.security.demo.security.JwtAuthenticationProvider;
import com.spring.security.demo.security.JwtLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
 private UserDetailsService userDetailsService;
 @Override
 public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义登录身份认证组件----> 指定了自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
 }
    @Override
 protected void configure(HttpSecurity http) throws Exception {
        // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
 http.cors().and().csrf().disable()
            .authorizeRequests()
            // 跨域预检请求
 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            // 登录URL
 .antMatchers("/login").permitAll()
            // swagger
 .antMatchers("/swagger**/**").permitAll()
            .antMatchers("/webjars/**").permitAll()
            .antMatchers("/v2/**").permitAll()
            // 其他所有请求需要身份认证
 .anyRequest().authenticated();
 // 退出登录处理器
 http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
 // 开启登录认证流程过滤器----> 指定了登录认证流程过滤器 JwtLoginFilter,由它来触发登录认证
 http.addFilterBefore(new JwtLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
 // 访问控制时登录状态检查过滤器----> 指定了访问控制过滤器 JwtAuthenticationFilter,在授权时解析令牌和设置登录状态
 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
 }
    @Bean
 @Override public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
 }
    
}

登录认证触发过滤器

JwtLoginFilter 是在通过访问 /login 的POST请求是被首先被触发的过滤器,默认实现是 UsernamePasswordAuthenticationFilter,它继承了 AbstractAuthenticationProcessingFilter,抽象父类的 doFilter 定义了登录认证的大致操作流程,这里我们的 JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilter,并进行了两个主要内容的定制。

  1. 覆写认证方法,修改用户名、密码的获取方式,具体原因看代码注释
  2. 覆写认证成功后的操作,移除后台跳转,添加生成令牌并返回给客户端

JwtLoginFilter.java

package com.spring.security.demo.security;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.spring.security.demo.utils.HttpUtils;
import com.spring.security.demo.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
/**
 * 启动登录认证流程过滤器
 * @author Wulongbo
 * @date Nov 28, 2020
 */public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
   
   public JwtLoginFilter(AuthenticationManager authManager) {
        setAuthenticationManager(authManager);
 }
   
   @Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
         throws IOException, ServletException {
      // POST 请求 /login 登录时拦截, 由此方法触发执行登录认证流程,可以在此覆写整个登录认证逻辑
 super.doFilter(req, res, chain); 
}
   
   @Override
 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
      // 可以在此覆写尝试进行登录认证的逻辑,登录成功之后等操作不再此方法内
 // 如果使用此过滤器来触发登录认证流程,注意登录请求数据格式的问题
 // 此过滤器的用户名密码默认从request.getParameter()获取,但是这种
 // 读取方式不能读取到如 application/json 等 post 请求数据,需要把
 // 用户名密码的读取逻辑修改为到流中读取request.getInputStream()
 String body = getBody(request);
 JSONObject jsonObject = JSON.parseObject(body);
 String username = jsonObject.getString("username");
 String password = jsonObject.getString("password");
 if (username == null) {
         username = "";
 }
      if (password == null) {
         password = "";
 }
      username = username.trim();
 JwtAuthenticatioToken authRequest = new JwtAuthenticatioToken(username, password);
 // Allow subclasses to set the "details" property
 setDetails(request, authRequest);
 return this.getAuthenticationManager().authenticate(authRequest);
 }
   
   @Override
 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
 Authentication authResult) throws IOException, ServletException {
      // 存储登录认证信息到上下文
 SecurityContextHolder.getContext().setAuthentication(authResult);
 // 记住我服务
 getRememberMeServices().loginSuccess(request, response, authResult);
 // 触发事件监听器
 if (this.eventPublisher != null) {
         eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
 }
      // 生成并返回token给客户端,后续访问携带此token
 JwtAuthenticatioToken token = new JwtAuthenticatioToken(null, null, JwtTokenUtils.generateToken(authResult));
 HttpUtils.write(response, token);
 }
   
   /** 
 * 获取请求Body
 * @param request
 * @return
 */
 public String getBody(HttpServletRequest request) {
      StringBuilder sb = new StringBuilder();
 InputStream inputStream = null;
 BufferedReader reader = null;
 try {
         inputStream = request.getInputStream();
 reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
 String line = "";
 while ((line = reader.readLine()) != null) {
            sb.append(line);
 }
      } catch (IOException e) {
         e.printStackTrace();
 } finally {
         if (inputStream != null) {
            try {
               inputStream.close();
 } catch (IOException e) {
               e.printStackTrace();
 }
         }
         if (reader != null) {
            try {
               reader.close();
 } catch (IOException e) {
               e.printStackTrace();
 }
         }
      }
      return sb.toString();
 }
}

登录控制器

除了使用上面的登录认证过滤器拦截 /login Post请求之外,我们也可以不使用上面的过滤器,通过自定义登录接口实现,只要在登录接口手动触发登录流程并生产令牌即可。

其实 Spring Security 的登录认证过程只需调用 AuthenticationManager 的 authenticate(Authentication authentication) 方法,最终返回认证成功的 Authentication 实现类并存储到SpringContexHolder 上下文即可,这样后面授权的时候就可以从 SpringContexHolder 中获取登录认证信息,并根据其中的用户信息和权限信息决定是否进行授权。
LoginController.java

package com.spring.security.demo.controller;
import com.spring.security.demo.security.JwtAuthenticatioToken;
import com.spring.security.demo.utils.SecurityUtils;
import com.spring.security.demo.vo.HttpResult;
import com.spring.security.demo.vo.LoginBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
 * @Author wulongbo
 * @Date 2020/11/24 10:05
 * @Version 1.0
 */
 @RestController
 public class LoginController {
    @Autowired
 private AuthenticationManager authenticationManager;
 /**
 * 登录接口
 */
 @PostMapping(value = "/login")
    public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
        String username = loginBean.getUsername();
 String password = loginBean.getPassword();
 // 系统登录认证
 JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
 return HttpResult.ok(token);
 }
}

如下是登录认证的逻辑, 可以看到部分逻辑跟上面的登录认证过滤器差不多。

  1. 执行登录认证过程,通过调用 AuthenticationManager 的 authenticate(token) 方法实现
  2. 将认证成功的认证信息存储到上下文,供后续访问授权的时候获取使用
  3. 通过JWT生成令牌并返回给客户端,后续访问和操作都需要携带此令牌

有关登录过程的逻辑,参见SecurityUtils的login方法。

SecurityUtils.java

package com.spring.security.demo.utils;
import javax.servlet.http.HttpServletRequest;
import com.spring.security.demo.security.JwtAuthenticatioToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
/**
 * Security相关操作
 * @author Wulongbo
 * @date Jun 29, 2020
 */public class SecurityUtils {
   /**
 * 系统登录认证
 * @param request
 * @param username
 * @param password
 * @param authenticationManager
 * @return
 */
 public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
      JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
 token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
 // 执行登录认证过程
 Authentication authentication = authenticationManager.authenticate(token);
 // 认证成功存储认证信息到上下文
 SecurityContextHolder.getContext().setAuthentication(authentication);
 // 生成令牌并返回给客户端
 token.setToken(JwtTokenUtils.generateToken(authentication));
 return token;
 }
   /**
 * 获取令牌进行认证
 * @param request
 */
 public static void checkAuthentication(HttpServletRequest request) {
      // 获取令牌并根据令牌获取登录认证信息
 Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
 // 设置登录认证信息到上下文
 SecurityContextHolder.getContext().setAuthentication(authentication);
 }
   /**
 * 获取当前用户名
 * @return
 */
 public static String getUsername() {
      String username = null;
 Authentication authentication = getAuthentication();
 if(authentication != null) {
         Object principal = authentication.getPrincipal();
 if(principal != null && principal instanceof UserDetails) {
            username = ((UserDetails) principal).getUsername();
 }
      }
      return username;
 }
   
   /**
 * 获取用户名
 * @return
 */
 public static String getUsername(Authentication authentication) {
      String username = null;
 if(authentication != null) {
         Object principal = authentication.getPrincipal();
 if(principal != null && principal instanceof UserDetails) {
            username = ((UserDetails) principal).getUsername();
 }
      }
      return username;
 }
   
   /**
 * 获取当前登录信息
 * @return
 */
 public static Authentication getAuthentication() {
      if(SecurityContextHolder.getContext() == null) {
         return null;
 }
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 return authentication;
 }
   
}

令牌生成器

我们令牌是使用JWT生成的,令牌生成的逻辑,参见源码JwtTokenUtils的generateToken相关方法。

JwtTokenUtils.java

package com.spring.security.demo.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.spring.security.demo.security.GrantedAuthorityImpl;
import com.spring.security.demo.security.JwtAuthenticatioToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
 * JWT工具类
 * @author Wulongbo
 * @date Jun 29, 2020
 */public class JwtTokenUtils implements Serializable {
   private static final long serialVersionUID = 1L;
 /**
 * 用户名称
 */
 private static final String USERNAME = Claims.SUBJECT;
 /**
 * 创建时间
 */
 private static final String CREATED = "created";
 /**
 * 权限列表
 */
 private static final String AUTHORITIES = "authorities";
 /**
 * 密钥
 */
 private static final String SECRET = "abcdefgh";
 /**
 * 有效期12小时
 */
 private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;
 /**
 * 生成令牌
 *
 * @param authentication 用户
 * @return 令牌
 */
 public static String generateToken(Authentication authentication) {
       Map<String, Object> claims = new HashMap<>(3);
 claims.put(USERNAME, SecurityUtils.getUsername(authentication));
 claims.put(CREATED, new Date());
 claims.put(AUTHORITIES, authentication.getAuthorities());
 return generateToken(claims);
 }
   /**
 * 从数据声明生成令牌
 *
 * @param claims 数据声明
 * @return 令牌
 */
 private static String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
 return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
 }
    /**
 * 从令牌中获取用户名
 *
 * @param token 令牌
 * @return 用户名
 */
 public static String getUsernameFromToken(String token) {
       String username;
 try {
           Claims claims = getClaimsFromToken(token);
 username = claims.getSubject();
 } catch (Exception e) {
           username = null;
 }
       return username;
 }
   
   /**
 * 根据请求令牌获取登录认证信息
 * @param token 令牌
 * @return 用户名
 */
 public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
      Authentication authentication = null;
 // 获取请求携带的令牌
 String token = JwtTokenUtils.getToken(request);
 if(token != null) {
         // 请求令牌不能为空
 if(SecurityUtils.getAuthentication() == null) {
            // 上下文中Authentication为空
 Claims claims = getClaimsFromToken(token);
 if(claims == null) {
               return null;
 }
            String username = claims.getSubject();
 if(username == null) {
               return null;
 }
            if(isTokenExpired(token)) {
               return null;
 }
            Object authors = claims.get(AUTHORITIES);
 List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
 if (authors != null && authors instanceof List) {
               for (Object object : (List) authors) {
                  authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
 }
            }
            authentication = new JwtAuthenticatioToken(username, null, authorities, token);
 } else {
            if(validateToken(token, SecurityUtils.getUsername())) {
               // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
 authentication = SecurityUtils.getAuthentication();
 }
         }
      }
      return authentication;
 }
   /**
 * 从令牌中获取数据声明
 *
 * @param token 令牌
 * @return 数据声明
 */
 private static Claims getClaimsFromToken(String token) {
        Claims claims;
 try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
 } catch (Exception e) {
            claims = null;
 }
        return claims;
 }
    /**
 * 验证令牌
 * @param token
 * @param username
 * @return
 */
 public static Boolean validateToken(String token, String username) {
       String userName = getUsernameFromToken(token);
 return (userName.equals(username) && !isTokenExpired(token));
 }
   /**
 * 刷新令牌
 * @param token
 * @return
 */
 public static String refreshToken(String token) {
       String refreshedToken;
 try {
           Claims claims = getClaimsFromToken(token);
 claims.put(CREATED, new Date());
 refreshedToken = generateToken(claims);
 } catch (Exception e) {
           refreshedToken = null;
 }
       return refreshedToken;
 }
   /**
 * 判断令牌是否过期
 *
 * @param token 令牌
 * @return 是否过期
 */
 public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
 Date expiration = claims.getExpiration();
 return expiration.before(new Date());
 } catch (Exception e) {
            return false;
 }
    }
    /**
 * 获取请求token
 * @param request
 * @return
 */
 public static String getToken(HttpServletRequest request) {
       String token = request.getHeader("Authorization");
 String tokenHead = "Bearer ";
 if(token == null) {
           token = request.getHeader("token");
 } else if(token.contains(tokenHead)){
           token = token.substring(tokenHead.length());
 } 
        if("".equals(token)) {
           token = null;
 }
        return token;
 }
}

登录身份认证组件

上面说到登录认证是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的,而 AuthenticationManager 又是通过调用 AuthenticationProvider 的 authenticate(Authentication authentication) 来完成认证的,所以通过定制 AuthenticationProvider 也可以完成各种自定义的需求,我们这里只是简单的继承 DaoAuthenticationProvider 展示如何自定义,具体的大家可以根据各自的需求按需定制。

JwtAuthenticationProvider.java

package com.spring.security.demo.security;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
 * 身份验证提供者
 * @author Wulongbo
 * @date Nov 20, 2020
 */public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
    public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
        setUserDetailsService(userDetailsService);
 setPasswordEncoder(new BCryptPasswordEncoder());
 }
    @Override
 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       // 可以在此处覆写整个登录认证逻辑
 return super.authenticate(authentication);
 }
    
    @Override
 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
         throws AuthenticationException {
       // 可以在此处覆写密码验证逻辑
 super.additionalAuthenticationChecks(userDetails, authentication);
 }
}

我们自定义的 UserDetailsService,从我们的用户服务 UserService 中获取用户和权限信息。

UserDetailsServiceImpl.java

package com.spring.security.demo.security;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.spring.security.demo.model.User;
import com.spring.security.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
 * 用户登录认证信息查询
 * @author Wulongbo
 * @date Jun 29, 2020
 */@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
 private UserService userService;
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
 if (user == null) {
            throw new UsernameNotFoundException("该用户不存在");
 }
        // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
 Set<String> permissions = userService.findPermissions(username);
 List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
 return new JwtUserDetails(username, user.getPassword(), grantedAuthorities);
 }
}

用户认证信息

上面 UserDetailsService 加载好用户认证信息后会封装认证信息到一个 UserDetails 的实现类。

默认实现是 User 类,我们这里没有特殊需要,简单继承即可,复杂需求可以在此基础上进行拓展。

JwtUserDetails.java

package com.spring.security.demo.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
/**
 * 安全用户模型
 * @author Wulongbo
 * @date Jun 29, 2020
 */public class JwtUserDetails extends User {
   private static final long serialVersionUID = 1L;
 public JwtUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
      this(username, password, true, true, true, true, authorities);
 }
   
   public JwtUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
 boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
      super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
 }
}

用户操作代码

简单的用户模型,包含用户名密码。

User.java

package com.spring.security.demo.model;
import lombok.Data;
/**
 * 用户模型
 * @author Wulongbo
 * @date Jun 29, 2020
 */@Data
public class User {
    private Long id;
 private String username;
 private String password;
}

用户服务接口,只提供简单的用户查询和权限查询接口用于模拟。

UserService.java

package com.spring.security.demo.service;
import com.spring.security.demo.model.User;
import java.util.Set;
/**
 * 用户管理
 * @author Wulongbo
 * @date Jun 29, 2020
 */public interface UserService {
   /**
 * 根据用户名查找用户
 * @param username
 * @return
 */
 User findByUsername(String username);
 /**
 * 查找用户的菜单权限标识集合
 * @param username
 * @return
 */
 Set<String> findPermissions(String username);
}

用户服务实现,只简单获取返回模拟数据,实际场景根据情况从DAO获取即可。

SysUserServiceImpl.java

package com.spring.security.demo.service.impl;
import java.util.HashSet;
import java.util.Set;
import com.spring.security.demo.model.User;
import com.spring.security.demo.service.UserService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements UserService {
   @Override
 public User findByUsername(String username) {
      User user = new User();
 user.setId(1L);
 user.setUsername(username);
 String password = new BCryptPasswordEncoder().encode("123456");
 user.setPassword(password);
 return user;
 }
   @Override
 public Set<String> findPermissions(String username) {
      Set<String> permissions = new HashSet<>();
 permissions.add("sys:user:view");
 permissions.add("sys:user:add");
 permissions.add("sys:user:edit");
 return permissions;
 }
}

用户控制器,提供三个测试接口,其中权限列表中未包含删除接口定义的权限('sys:user:delete'),登录之后也将无权限调用。

UserController.java

package com.spring.security.demo.controller;
import com.spring.security.demo.vo.HttpResult;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 用户控制器
 * @author Wulongbo
 * @date Jun 29, 2020
 */
@RestController
@RequestMapping("user")
public class UserController {
    
    @PreAuthorize("hasAuthority('sys:user:view')")
    @GetMapping(value="/findAll")
    public HttpResult findAll() {
        return HttpResult.ok("the findAll service is called success.");
 }
    
    @PreAuthorize("hasAuthority('sys:user:edit')")
    @GetMapping(value="/edit")
    public HttpResult edit() {
        return HttpResult.ok("the edit service is called success.");
 }
    
    @PreAuthorize("hasAuthority('sys:user:delete')")
    @GetMapping(value="/delete")
    public HttpResult delete() {
        return HttpResult.ok("the delete service is called success.");
 }
}

登录认证检查过滤器

访问接口的时候,登录认证检查过滤器 JwtAuthenticationFilter 会拦截请求校验令牌和登录状态,并根据情况设置登录状态。

JwtAuthenticationFilter.java

package com.spring.security.demo.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.spring.security.demo.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
/**
 * 登录认证检查过滤器
 * @author Wulongbo
 * @date Jun 29, 2020
 */public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
   
   @Autowired
 public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
 }
    @Override
 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
       // 获取token, 并检查登录状态
 SecurityUtils.checkAuthentication(request);
 chain.doFilter(request, response);
 }
    
}

具体详细获取token和检查登录状态代码请查看SecurityUtils的checkAuthentication方法。

编译测试运行

  •  项目目录结构如下,Maven install,开始执行Maven构建,如果出现如下信息,就说明项目编译打包成功了。

目录结构

  • 打开文件文件 DemoApplication.java -> Run DemoApplication,开始启动应用,当出现如下信息的时候,就说明应用启动成功了,默认启动端口是8080

启动项目

顺利启动
如果出现:

Caused by: java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*"since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.

则全局的跨域配置有问题,个人的简单解决方案是注释掉CorsConfig配置类,在需要的Controller上加上@CrossOrigin注解,使该控制页面支持跨域请求,但存在的问题就是不能统一配置,需要每个控制页面都加入@CrossOrigin注解。

如果项目中集成了activiti,则需要在springboot的启动类中排除底下依赖即可

@SpringBootApplication(exclude = {
      org.activiti.spring.boot.SecurityAutoConfiguration.class, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class
})

我们先再未登录没有令牌的时候直接访问接口,发现都返回无权限,禁止访问的结果。表示因为权限的问题拒绝访问。
403禁止访问

打开 LoginController,输入我们用户名和密码(username:"任何字符,因为没走库,啊哈哈", password:123456,密码是我们在SysUserServiceImpl中设置的,实际业务通过数据库做登录认证就好,示例为伪代码,只体现逻辑即可)登录成功之后,会成功返回令牌,如下图所示。
登录login

拷贝返回的令牌,粘贴到令牌参数输入框,再次访问 /user/edit 接口。这个时候,成功的返回了结果: the edit service is called success.
携带token后有权限访问

同样的,拷贝返回的令牌,粘贴到令牌参数输入框,访问 /user/delete 接口。发现还是返回拒绝访问的结果,那是因为访问这个接口需要 'sys:user:delete' 权限,而我们之前返回的权限列表中并没有包含,所以授权访问失败。
无权限访问

我们可以修改一下 SysUserServiceImpl,添加上‘sys:user:delete’ 权限,重新登录,再次访问一遍。
permissions.add("sys:user:delete");
添加权限
发现删除接口也可以访问了,记住务必要重新调用登录接口,获取令牌后拷贝到删除接口,再次访问删除接口。
访问通过
到此,一个简单但相对完整的Spring Security案例就实现了,我们通过Spring Security实现了简单的登录认证和访问控制。

权限认证逻辑解读

关闭登录认证流程过滤器JwtLoginFilter,使用自定义登录接口实现,开启JwtAuthenticationFilter,在授权时解析令牌和设置登录状态

  1. 在访问LoginControllerlogin接口的时候,登录认证检查过滤器JwtAuthenticationFilter,会拦截login Post请求,这里登录接口是没有传递登录令牌的,所以【获取令牌并根据令牌获取登录认证信息】为null,SecurityContextHolder上下文登录认证信息也是null。
  2. 检查过滤器拦截后,跳转到刚刚的登录接口,执行登录认证过程,通过调用【authenticationManager.authenticate(token)】方法实现。该方法会跳转到UserDetailsService的实现类即我们自定义的UserDetailsServiceImpl中,该方法注入了UserService接口,进而从我们的用户服务 UserService 中获取用户和权限信息,示例中权限为代码中写定的Set集合,通过stream()流操作
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());

将我们集合转换成对象数组,实际应用中可以通过UserService去数据库中查找用户的菜单权限标识集合。需要的依赖:

<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <scope>runtime</scope>
</dependency>
<!-- 引入阿里数据库连接池 -->
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid</artifactId>
 <version>1.1.6</version>
</dependency>

认证成功后,存储认证信息到上下文,供后续访问授权的时候获取使用。

SecurityContextHolder.getContext().setAuthentication(authentication);

此时SecurityContextHolder上下文登录认证信息便记录的用户的权限列表。
接下来只要把生成的token令牌【数据声明】并返回给客户端,后续访问和操作都需要携带此令牌,便可实现控制用户的权限访问。


isWulongbo
228 声望26 粉丝

在人生的头三十年,你培养习惯,后三十年,习惯铸就你