可能有些人会觉得这篇似曾相识,没错,这篇是由原文章进行二次开发的。

前阵子有些事情,但最近看到评论区说原文章最后实现的是单模块的验证,由于过去太久也懒得验证,所以重新写了一个完整的可以跑得动的一个。

OK,回到正题,以下是真正对应的微服务多模块的一个方法,使用到的技术有:基于微服务的Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT。

对使用到的微服务技术进行在项目中的说明:

Security:负责登录验证(文章中没有实现授权,在过滤器中直接返回null,如果想实现授权,可以在返回null的地方添加授权信息类似ROLE_ADMIN,同时在Security的配置文件那里添加授权信息即可)。

Redis:负责缓存token跟用户数据。

Gateway:对前端提供的接口,由它多个模块进行接口调用。

OpenFeign:提供给security查询数据库中的用户信息。

Nacos:注册服务中心,注册服务的信息,使OpenFeign可以调用其他服务模块。

注意:虽然是原文章的二次编写,但是很多都不同,建议直接跟着这篇走。

目录

1.项目结构

2.Common模块

pom.xml

2.1 RedisConfig

2.2 RedisUtil

2.3 ResponseUtil

2.4  TokenUtil

         2.5  CorConfig

3.model模块

3.1 pom

3.2 User

         3.3 UserFeign

4.service模块

  4.1 目录结构​编辑

  4.2 service_user模块

    4.2.1 pom.xml

    4.2.2 application.yml

    4.2.3 Service_UserApp启动类

4.3 其他service模块

5.spring_security模块

5.1 pom

5.2 DiyUserDetails(UserDetails)

5.3 WebSecurityConfig(WebSecurityConfigurerAdapter)

5.4 TokenOncePerRequestFilter(OncePerRequestFilter)

5.5 LoginAuthenticationEntryPoint(AuthenticationEntryPoint)

5.6 LoginInFailHandler(AuthenticationFailureHandler)

5.7 LoginInSuccessHandler(AuthenticationSuccessHandler)

5.8 LogOutSuccessHandler(LogoutSuccessHandler)

5.9 NothingAccessDeniedHandler(AccessDeniedHandler)

         5.10 MyUserDetailService(UserDetailsService)

6.gateway模块

6.1 pom

6.2 application.yml

7.测试

1.项目结构

涉及的模块有

(1)common(Redis配置文件、Redis工具、Token工具、返回给前端信息的工具;即如下文件RedisConfig、RedisUtil、TokenUtil、ResponseUtil);

(2)gateway;

(3)model(实体类,Feign的客户端);

(4)service(用户模块、课程模块);

(5)spring_security(security的过滤器跟配置文件)。

下面小编将全部一一介绍并且源码展示出来。

2.Common模块

pom.xml

org.springframework.boot

spring-boot-starter-data-redis

io.jsonwebtoken

jjwt

com.baomidou

mybatis-plus-boot-starter

org.projectlombok

lombok

org.springframework.boot

spring-boot-starter-web

com.github.xiaoymin

knife4j-spring-boot-starter

com.alibaba

fastjson

com.goyes

model

1.0-SNAPSHOT

compile

2.1 RedisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;

import com.fasterxml.jackson.annotation.PropertyAccessor;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.cache.annotation.CachingConfigurerSupport;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;

/*

* Redis配置

* 解决redis在业务逻辑处理层上不出错,缓存序列化问题

* */

@Configuration

@EnableCaching

public class RedisConfig extends CachingConfigurerSupport {

@Resource

RedisConnectionFactory redisConnectionFactory;

@Bean

public RedisTemplate redisTemplate(){

RedisTemplate redisTemplate=new RedisTemplate<>();

redisTemplate.setConnectionFactory(redisConnectionFactory);

//Json序列化配置

//1、String的序列化

StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();

// key采用String的序列化方式

redisTemplate.setKeySerializer(stringRedisSerializer);

// hash的key也采用String的序列化方式

redisTemplate.setHashKeySerializer(stringRedisSerializer);

//2、json解析任意的对象(Object),变成json序列化

Jackson2JsonRedisSerializer serializer=new Jackson2JsonRedisSerializer(Object.class);

ObjectMapper mapper=new ObjectMapper(); //用ObjectMapper进行转义

mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

//该方法是指定序列化输入的类型,就是将数据库里的数据按照一定类型存储到redis缓存中。

mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

serializer.setObjectMapper(mapper);

// value序列化方式采用jackson

redisTemplate.setValueSerializer(serializer);

// hash的value序列化方式采用jackson

redisTemplate.setHashValueSerializer(serializer);

return redisTemplate;

}

}

2.2 RedisUtil

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

import java.time.LocalDateTime;

import java.time.format.DateTimeFormatter;

import java.time.temporal.ChronoUnit;

import java.util.concurrent.TimeUnit;

@Component

public class RedisUtil {

@Autowired

private StringRedisTemplate stringRedisTemplate;

public static StringRedisTemplate stringRedisTemplateStatic;

@PostConstruct //在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。

public void initStringRedisTemplate(){

stringRedisTemplateStatic=this.stringRedisTemplate;

}

private static final DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

/*

* 保存token信息到redis,也可直接在创建token中使用该方法

* */

public static void redis_SaveTokenInfo(String token,String username){

//以username做key

LocalDateTime localDateTime=LocalDateTime.now();

stringRedisTemplateStatic.opsForHash().put(username,"token",token);

stringRedisTemplateStatic.opsForHash().put(username,"refreshTime", //有效时间

df.format(localDateTime.plus(7*24*60*60*1000, ChronoUnit.MILLIS)));

stringRedisTemplateStatic.opsForHash().put(username,"expiration", //过期时间 5分钟 300秒

df.format(localDateTime.plus(300*1000, ChronoUnit.MILLIS)));

stringRedisTemplateStatic.expire(username,7*24*60*60*1000, TimeUnit.SECONDS);

}

/*

* 检查redis是否存在token

* */

public static boolean hasToken(String username){

return stringRedisTemplateStatic.opsForHash().getOperations().hasKey(username);

}

}

2.3 ResponseUtil

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.Data;

import org.apache.ibatis.annotations.Result;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

import java.util.HashMap;

import java.util.Map;

@Data

public class ResponseUtil {

public static int OK = 200;

public static int ERROR = 404;

public static String SUCCESS="操作成功!";

public static String NO_SUCCESS="操作失败,请稍候重试。";

//返回码(200)

private int code;

//返回消息

private String message;

@ApiModelProperty(value = "返回数据(单条或多条)")

private Map data = new HashMap();

public ResponseUtil(int code, String message) {

this.code=code;

this.message=message;

}

public ResponseUtil(int code, String message, Map data) {

this.code=code;

this.message=message;

this.data=data;

}

//对response写入Object数据

public static void reponseOutDiy(HttpServletResponse response,int statusCode , Object result) {

ObjectMapper mapper = new ObjectMapper();

PrintWriter writer = null;

response.setStatus(statusCode);

response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

try {

writer = response.getWriter();

mapper.writeValue(writer, result);

writer.flush();

} catch (IOException e) {

e.printStackTrace();

} finally {

if (writer != null) {

writer.flush();

writer.close();

}

}

}

}

2.4  TokenUtil

import com.Lino_white.model.User; //model模块的user

import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.Locale;

public class TokenUtil {

public static final String APP_SECRET ="Lino_white"; //随便取,你的Token密钥

public static final String TOKEN_HEAD="Authorization";

public static final String TOKEN_PREFIX = "Bearer ";

public static String createToken(User user){

String token = Jwts.builder()

.setId(String.valueOf(user.getId()))

.setSubject(user.getUsername())

.setIssuedAt(new Date()) //签发时间

.setIssuer("Lino_white") //签发者

.setExpiration(new Date(System.currentTimeMillis() + 300* 1000)) //过期时间 5分钟 自行设置

.signWith(SignatureAlgorithm.HS256, APP_SECRET) //签名算法跟密钥

.claim("identity", user.getIdentity()) //可添加额外的属性

.compact();

return token;

}

//重新生成新的Token,异常时间由传入的参数决定

public static String createToken(User user,Date expirationTime){

SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

try {

expirationTime= (Date) f.parse(f.format(expirationTime));

} catch (ParseException e) {

throw new RuntimeException(e);

}

String token = Jwts.builder()

.setId(String.valueOf(user.getId()))

.setSubject(user.getUsername())

.setIssuedAt(new Date()) //签发时间

.setIssuer("Lino_white") //签发者

.setExpiration(expirationTime) //过期时间

.signWith(SignatureAlgorithm.HS256, APP_SECRET) //签名算法跟密钥

.claim("identity", user.getIdentity()) //可添加额外的属性

.compact();

return token;

}

//获得用户名

public String getUsernameFromToken(String token){

return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getSubject();

}

/**

* 判断token是否存在与有效(1)

*/

public boolean checkToken(String token){

if (StringUtils.isEmpty(token)) return false;

try {

Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);

}catch (Exception e){

e.printStackTrace();

return false;

}

return true;

}

/**

* 判断token是否存在与有效(2)

*/

public boolean checkToken(HttpServletRequest request){

try {

String token = request.getHeader("token");

return checkToken(token);

}catch (Exception e){

e.printStackTrace();

return false;

}

}

//获得全部属性

public Claims parseJwt(String token){

Claims claims = Jwts.parser()

.setSigningKey(APP_SECRET) // 设置标识名

.parseClaimsJws(token) //解析token

.getBody();

return claims;

}

//获得指定属性

public String getTokenClaim(String token,String key){

Claims body = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();

return String.valueOf(body.get(key));

}

}

2.5  CorConfig

package com.goyes.common.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.format.FormatterRegistry;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**

* 解决跨域

* @author: white

*/

@Configuration

public class CorConfig implements WebMvcConfigurer {

@Override

public void addCorsMappings(CorsRegistry registry) {

System.out.println("开始解决跨域");

registry.addMapping("/**")

.allowedOrigins("*")

.allowedMethods("*")

// .allowCredentials(true)//是否有验证,有就打开

.allowedHeaders("*")

.maxAge(3600);

}

}

3.model模块

3.1 pom

org.springframework.cloud

spring-cloud-starter-openfeign

org.springframework.cloud

spring-cloud-starter-netflix-ribbon

com.baomidou

mybatis-plus-boot-starter

org.projectlombok

lombok

com.github.xiaoymin

knife4j-spring-boot-starter

3.2 User

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.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

import java.io.Serializable;

@AllArgsConstructor

@NoArgsConstructor

@Data

@ApiModel(value = "实体:用户")

@TableName("user")

public class User implements Serializable {

@ApiModelProperty("用户id")

@TableId(value = "id",type = IdType.AUTO)

private int id;

@ApiModelProperty("用户名")

private String username;

@ApiModelProperty("密码")

private String password;

@TableField("identity")

@ApiModelProperty("身份")

private String identity;

@Override

public String toString() {

return "User{" +

"id=" + id +

", username='" + username + '\'' +

", password='" + password + '\'' +

", identity='" + identity + '\'' +

'}';

}

}

3.3 UserFeign

package com.goyes.model.client;

import com.goyes.model.User;

import org.springframework.cloud.openfeign.FeignClient;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "service-user")

public interface UserFeign {

@GetMapping("/api/user/{username}")

public User findUserByName(@PathVariable("username") String username);

}

4.service模块

  4.1 目录结构

  4.2 service_user模块

    4.2.1 pom.xml

注意:service_user接入了security模块。

org.springframework.cloud

spring-cloud-starter-openfeign

com.alibaba.cloud

spring-cloud-starter-alibaba-nacos-discovery

com.alibaba.cloud

spring-cloud-starter-alibaba-nacos-config

com.alibaba.nacos

nacos-client

mysql

mysql-connector-java

com.baomidou

mybatis-plus-boot-starter

com.alibaba

fastjson

org.springframework.boot

spring-boot-starter-web

com.github.xiaoymin

knife4j-spring-boot-starter

org.springframework.boot

spring-boot-starter-test

org.springframework

spring-web

compile

com.goyes

model

1.0-SNAPSHOT

compile

com.goyes

common

1.0-SNAPSHOT

compile

com.goyes

service_other

1.0-SNAPSHOT

compile

com.goyes

service_course

1.0-SNAPSHOT

compile

com.goyes.service_comment

service_comment

1.0-SNAPSHOT

compile

com.goyes

spring_security

1.0-SNAPSHOT

    4.2.2 application.yml

server:

port: 8001

spring:

application:

name: service-user

main:

allow-bean-definition-overriding: true

profiles:

active: dev

cloud:

nacos:

config:

server-addr: 127.0.0.1:8848

group: dev

discovery:

cluster-name: WHITE

feign:

client:

config:

default:

connect-timeout: 10000

read-timeout: 10000

    4.2.3 Service_UserApp启动类

@SpringBootApplication

@EnableSwagger2WebMvc

@EnableDiscoveryClient

@EnableFeignClients

@EnableCaching

public class Service_UserApp

{

public static void main( String[] args )

{

SpringApplication.run(Service_UserApp.class, args);

}

}

    4.2.4 ApiController控制器

在任意一个控制器中,添加如下代码,该接口将用于OpenFeign的远程接口调用,由security模块中的自定义类MyUserDetailService去进行调用。(MyUserDetailService的代码在介绍security模块中会出现)

@GetMapping("/api/user/{username}")

public User findUserByName(@PathVariable("username") String username){

User userByName = userService.findUserByName(username);

return userByName;

}

4.3 其他service模块

对于其他模块,相对应跟service_user模块一样,进行如下操作即可:

(1)在pom.xml中引入security模块

com.goyes

spring_security

1.0-SNAPSHOT

(2)在application.xml中添加以下代码

spring:

main:

allow-bean-definition-overriding: true

        防止出现运行异常报错信息,对于同一个服务的FeignClient来说,配置该属性不会造成覆盖,详情可以查看该文章:Consider renaming one of the beans:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

5.spring_security模块

5.1 pom

org.springframework.cloud

spring-cloud-starter-openfeign

org.springframework.cloud

spring-cloud-starter-netflix-ribbon

mysql

mysql-connector-java

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-data-redis

com.alibaba

fastjson

com.goyes

model

1.0-SNAPSHOT

compile

com.goyes

common

1.0-SNAPSHOT

compile

5.2 DiyUserDetails(UserDetails)

import com.Lino_white.model.User;

import lombok.Data;

import lombok.EqualsAndHashCode;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.util.StringUtils;

import java.io.Serializable;

import java.util.ArrayList;

import java.util.Collection;

@Data

@EqualsAndHashCode(callSuper = false)

public class DiyUserDetails extends User implements UserDetails, Serializable {

//用户权限列表

private Collection authorities;

@Override

public Collection getAuthorities() {

Collection authorities1 = new ArrayList<>();

for(String permissionValue : authorities) {

if(StringUtils.isEmpty(permissionValue)) continue;

SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);

authorities1.add(authority);

}

return authorities1;

}

@Override

public boolean isAccountNonExpired() {

return true;

}

@Override

public boolean isAccountNonLocked() {

return true;

}

@Override

public boolean isCredentialsNonExpired() {

return true;

}

@Override

public boolean isEnabled() {

return true;

}

}

5.3 WebSecurityConfig(WebSecurityConfigurerAdapter)

注意:前面将对service_user的远程接口定义为/api/user/{username},所以在过滤方面要放行该路径,否则security无法调用数据库查询用户信息,导致程序报错。

对此,可以查看该文章feign.FeignException$Unauthorized

package com.goyes.spring_security.config;

import com.goyes.spring_security.filter.TokenAuthenticationFilter;

import com.goyes.spring_security.filter.TokenLoginFilter;

import com.goyes.spring_security.filter.TokenOncePerRequestFilter;

import com.goyes.spring_security.handler.*;

import com.goyes.spring_security.service.MyUserDetailService;

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.userdetails.UserDetailsService;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.web.cors.CorsUtils;

@Configuration

@EnableWebSecurity //开启Security功能

@EnableGlobalMethodSecurity(prePostEnabled = true) //启动方法级别的权限认证

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired

private MyUserDetailService myUserDetailService;

@Bean

//配置密码加密器

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

//配置哪些请求不拦截

//TODO 将需要Feign的方法前缀都用上api,得到api/select/user/{user_id}这样的路径不受限制

// 由于api路径是由服务模块自己去调用的,所以gateway不用做路径请求的处理

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/api/**","/doc.html#/**","/swagger-resources");

}

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());

}

//配置安全策略

@Override

protected void configure(HttpSecurity http) throws Exception {

System.out.println("读取配置*****************WHITE");

http.authorizeRequests()

.anyRequest().authenticated()

.and()

//该过滤器设置在用户名、密码、权限过滤器之前。这样每次访问接口都会经过此过滤器,我们可以获取请求路径,并判定当请求路径为/login时进入验证码验证流程。

// 使用jwt的Authentication,来解析过来的请求是否有token

.addFilterBefore(new TokenOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class)

//登录后,访问没有权限处理类

.exceptionHandling().accessDeniedHandler(new NothingAccessDeniedHandler())

//匿名访问,没有权限的处理类

.authenticationEntryPoint(new LoginAuthenticationEntryPoint())

.and()

.formLogin()

.successHandler(new LoginInSuccessHandler())

.failureHandler(new LoginInFailHandler())

.and()

.logout()

.logoutSuccessHandler(new LogOutSuccessHandler())

// 配置取消session管理

.and()

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()

.csrf().disable();

}

}

5.4 TokenOncePerRequestFilter(OncePerRequestFilter)

注意:在这里TokenUtil跟RedisUtil对于过期时间的定义不同。

token过期时间为3分钟,redis上存储的异常时间为5分钟,并且redis上存储的刷新时间为7天

在下面的配置文件中,仅仅对token进行分析而已,可以根据需要在这里做验证码校验。

token的过期时间在以下代码中是这样做的,当token过期时间3分钟到了,判断redis上存储的异常时间是否到了5分钟,没到5分钟就返回一个新的token给前端,前端拿到该token就可以继续访问;如果到了5分钟,则会停止访问并通知前端 “用户已经过期,请重新登录”。

小编有个想法(还没做):在这里可以重新定义过期时间,比如用户每次访问时都进行判断:当token的过期时间小于1分钟后就刷新redis的异常时间,这样可以使当token要过期时,就有新的token出现,但这样操作也存在缺点:就是要消耗内存资源,每次都得去读取token是否临近过期时间了。对于这块,可以针对自己的情况去做调整。

package com.goyes.spring_security.filter;

import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.JSONObject;

import com.fasterxml.jackson.annotation.JsonFormat;

import com.goyes.common.utils.RedisUtil;

import com.goyes.common.utils.ResponseUtil;

import com.goyes.common.utils.TokenUtil;

import com.goyes.model.User;

import com.goyes.spring_security.model.DiyUserDetails;

import io.github.classgraph.json.JSONUtils;

import io.jsonwebtoken.Claims;

import io.jsonwebtoken.ExpiredJwtException;

import jdk.nashorn.internal.parser.JSONParser;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.boot.json.JsonParser;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.GrantedAuthority;

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.stereotype.Component;

import org.springframework.web.filter.OncePerRequestFilter;

import springfox.documentation.spring.web.json.Json;

import sun.security.util.SecurityConstants;

//import sun.security.util.SecurityConstants;

import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.xml.crypto.Data;

import java.io.IOException;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Collection;

import java.util.Date;

/**

* 在用户名、密码、权限过滤器之前的过滤器

* 在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中

*

* TODO 下面过滤器仅做了针对token解析,包括token异常、过期、重新颁布等

* @author white

*/

@Component

public class TokenOncePerRequestFilter extends OncePerRequestFilter {

@Autowired

StringRedisTemplate stringRedisTemplate = RedisUtil.stringRedisTemplateStatic;

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

/*

* TODO 可在这里判断请求过来的路径是否为login,方式为post,来在这里进行验证码有效验证

* 验证成功则直接chain(request,response)继续走过滤

* */

String requestURI = request.getRequestURI();

System.out.println("开始请求,请求路径:"+requestURI+" 请求方式:"+request.getMethod());

User user = null;

SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

String authHeader = request.getHeader(TokenUtil.TOKEN_HEAD);

//没有token不用理

if (authHeader != null && authHeader.startsWith(TokenUtil.TOKEN_PREFIX)) {

final String authToken = authHeader.replace(TokenUtil.TOKEN_PREFIX, "");

//这里的authToken可能时间已过,需要重新创建一个token

//先对比redis中的过期时间,redis的过期时间随着用户的操作而更新,token可能没有及时更新

//判断是否一样,一样的话就是token失效了,跳转重新登录,

// 不一样就是redis过期时间更新了,生成新的token返回给前端

String username = null;

Claims claims;

try {

claims = new TokenUtil().parseJwt(authToken);

username = claims.getSubject();

} catch (ExpiredJwtException e) {

//token过期

claims = e.getClaims();

username = claims.getSubject();

user = JSONObject.parseObject(String.valueOf(stringRedisTemplate.opsForHash().get(username, "user")), User.class);

if (user == null) {

chain.doFilter(request, response);

return;

} else {

if (RedisUtil.hasToken(username)) {

Object expiration = stringRedisTemplate.opsForHash().get(username, "expiration");

Object tokenExpirationTime = f.format(claims.getExpiration());

Date expirationDate_redisTime = null, expirationDate_tokenTime = null, nowTime;

try {

expirationDate_redisTime = (Date) f.parseObject(String.valueOf(expiration));

expirationDate_tokenTime = (Date) f.parseObject(String.valueOf(tokenExpirationTime));

nowTime = (Date) f.parseObject(f.format(new Date()));

} catch (ParseException ex) {

throw new RuntimeException(ex);

}

System.out.println("*********Token过期(Start)***********");

System.out.println("token浏览器过期时间:" + tokenExpirationTime);

System.out.println("redis过期时间:" + expiration);

//

/*

* redis

* token

* */

if (expirationDate_redisTime.getTime() < expirationDate_tokenTime.getTime() ||

expirationDate_tokenTime.getTime() == expirationDate_redisTime.getTime() ||

expirationDate_redisTime.getTime() < nowTime.getTime()) {

//时间相同,跳转登录

ResponseUtil.reponseOutDiy(response, 401, "用户已过期,请重新登录");

System.out.println("*********Token过期(End)失效***********");

return;

} else {

//时间不同,生成新token 需要用户id,身份,用户名

//response存入token 返回

Object expiration_redisTime = stringRedisTemplate.opsForHash().get(username, "expiration");

Date date;

try {

date = (Date) f.parseObject(String.valueOf(expiration_redisTime));

} catch (ParseException ex) {

throw new RuntimeException(ex);

}

//通过数据库查询数据,创建token

System.out.println("这里之前开始的时间:" + date);

String token = TokenUtil.createToken(user, date);

System.out.println("—————————————————start—————————————————————");

System.out.println("token:" + token);

RedisUtil.redis_SaveTokenInfo(user, token);

response.setHeader(TokenUtil.TOKEN_HEAD, TokenUtil.TOKEN_PREFIX + token);

request.setAttribute(TokenUtil.TOKEN_HEAD, TokenUtil.TOKEN_PREFIX + token);

Date expiration1 = new TokenUtil().parseJwt(token).getExpiration();

System.out.println("重新更新token后过期时间:" + expiration1);

System.out.println("—————————————————End—————————————————————");

ResponseUtil.reponseOutDiy(response, 200, token);

System.out.println("*********Token过期(End)新Token***********");

return;

}

} else {

//TODO 新增,如果redis没有username,说明未登录

throw new RuntimeException("未登录");

}

}

}

//避免每次请求都请求数据库查询用户信息,从缓存中查询

user = JSONObject.parseObject(String.valueOf(stringRedisTemplate.opsForHash().get(username, "user")), User.class);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

if (user != null) {

UsernamePasswordAuthenticationToken authentication =

// TODO 未修改 这里的权限先空着

new UsernamePasswordAuthenticationToken(user, user.getPassword(), null);

authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);

}

}

}

System.out.println("走过滤——————————————————————————");

chain.doFilter(request, response);

}

}

5.5 LoginAuthenticationEntryPoint(AuthenticationEntryPoint)

import com.Lino_white.common.ResponseUtil;

import com.Lino_white.common.TokenUtil;

import jdk.nashorn.internal.parser.Token;

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;

/**

* 匿名未登录的时候访问,需要登录的资源的调用类

* @author Lino_white

*/

@Component

public class LoginAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override

public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {

String token =httpServletRequest.getHeader(TokenUtil.TOKEN_HEAD);

System.out.println("当前未登录,无法访问 ::"+token);

if (token!=null && token.contains(TokenUtil.TOKEN_PREFIX)) {

token=token.replace(TokenUtil.TOKEN_PREFIX,"");

String usernameFromToken = new TokenUtil().getUsernameFromToken(token);

System.out.println("用户名:"+usernameFromToken);

}

ResponseUtil.reponseOutDiy(httpServletResponse,401,"当前未登录,无法访问");

}

}

5.6 LoginInFailHandler(AuthenticationFailureHandler)

import com.Lino_white.common.ResponseUtil;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import org.springframework.stereotype.Component;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

/**

* 登录账号密码错误等情况下,会调用的处理类

* @author Lino_white

*/

@Component

public class LoginInFailHandler implements AuthenticationFailureHandler {

@Override

public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {

System.out.println("认证失败————————————");

ResponseUtil.reponseOutDiy(httpServletResponse,401,"登录失败,请重试");

}

}

5.7 LoginInSuccessHandler(AuthenticationSuccessHandler)

package com.goyes.spring_security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.goyes.common.utils.RedisUtil;

import com.goyes.common.utils.ResponseUtil;

import com.goyes.common.utils.TokenUtil;

import com.goyes.model.User;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

import java.time.LocalDateTime;

import java.util.HashMap;

import java.util.Map;

/**

* @LoginInSuccessHandler.java的作用:

* 登录成功处理类,登录成功后会调用里面的方法

* @author: white文

* @time: 2023/5/18 16:02

*/

@Slf4j

@Component

public class LoginInSuccessHandler implements AuthenticationSuccessHandler {

/**

* 用户通过TokenLoginFilter(UsernamePasswordAuthenticationFilter)后,

* 验证成功到这里进行

* 1.获取当前用户

* 2.token创建

* 3.并将其存入redis并返回

*/

@Override

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

log.info("登录成功,开始初始化token并缓存在redis");

User user =(User) authentication.getPrincipal();

String token = TokenUtil.createToken(user);

//redis缓存token

RedisUtil.redis_SaveTokenInfo(user,token);

//写入response

response.setHeader("token", TokenUtil.TOKEN_PREFIX+token);

try {

//登录成功,返回json格式进行提示

response.setContentType("application/json;charset=utf-8");

response.setStatus(HttpServletResponse.SC_OK);

PrintWriter out=response.getWriter();

Map map=new HashMap(4);

map.put("code",HttpServletResponse.SC_OK);

map.put("message","这里全部都是自定义的!登录成功");

map.put("token",token);

out.write(new ObjectMapper().writeValueAsString(map));

out.flush();

out.close();

}catch (Exception e){

e.printStackTrace();

}

}

}

5.8 LogOutSuccessHandler(LogoutSuccessHandler)

import com.Lino_white.common.RedisUtil;

import com.Lino_white.common.ResponseUtil;

import com.Lino_white.common.TokenUtil;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;

import org.springframework.stereotype.Component;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

@Component

public class LogOutSuccessHandler implements LogoutSuccessHandler {

private StringRedisTemplate stringRedisTemplate= RedisUtil.stringRedisTemplateStatic;

@Override

public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

//用户退出登录

System.out.println("LogoutSuccessHandler退出");

String token=request.getHeader("token");

if (token==null) token=request.getHeader(TokenUtil.TOKEN_HEAD);

token=token.replace(TokenUtil.TOKEN_PREFIX,"");

String username = new TokenUtil().getUsernameFromToken(token);

Authentication au = SecurityContextHolder.getContext().getAuthentication();

if (au!=null) new SecurityContextLogoutHandler().logout(request,response,au);

Boolean delete = stringRedisTemplate.delete(username);

if (delete) ResponseUtil.reponseOutDiy(response,200,"用户已成功退出");

}

}

5.9 NothingAccessDeniedHandler(AccessDeniedHandler)

import com.Lino_white.common.ResponseUtil;

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;

/**

* 没有权限,被拒绝访问时的调用类

* @author Lino_white

*/

@Component

public class NothingAccessDeniedHandler implements AccessDeniedHandler {

@Override

public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {

System.out.println("没有权限");

ResponseUtil.reponseOutDiy(httpServletResponse,403,"当前您没有该权限");

}

}

5.10 MyUserDetailService(UserDetailsService)

注意:在这里调用了model模块中的UserFeign文件,实现读取service_user模块中的findUserByName方法。

package com.goyes.spring_security.service;

import com.goyes.model.User;

import com.goyes.model.client.UserFeign;

import com.goyes.spring_security.model.DiyUserDetails;

import org.springframework.beans.BeanUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Service;

import java.util.ArrayList;

import java.util.List;

/**

* 从数据库读取用户信息(用户名,密码,身份)进行身份认证

*/

@Service

public class MyUserDetailService implements UserDetailsService{

@Autowired

private UserFeign userFeign;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

System.out.println("********开始loadUserByUsername********");

User user = userFeign.findUserByName(username);

System.out.println("浏览器的username:"+username);

if (user==null) throw new UsernameNotFoundException(username);

System.out.println("数据库的username:"+user.getUsername());

//根据当前用户名查询用户权限

List authorities=new ArrayList<>();

authorities.add("ROLE_"+user.getIdentity());

DiyUserDetails details=new DiyUserDetails();

BeanUtils.copyProperties(user,details);

details.setAuthorities(authorities);

//如果数据库密码无加密,用下列

//details.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));

System.out.println("********结束loadUserByUsername********");

return details;

}

}

6.gateway模块

6.1 pom

org.springframework.cloud

spring-cloud-starter-gateway

6.2 application.yml

server:

port: 10000

spring:

application:

name: gateway

cloud:

gateway:

routes:

- id: user

uri: http://localhost:8001

predicates:

- Path=/user/**,/admin/**,/api/user/**,/login,/logout

- id: course

uri: http://localhost:8002

predicates:

- Path=/course/**

7.测试

这里是使用postman工具进行测试的,在这里之前已经在数据库有用户名跟密码都为111的数据,并且密码已是加密形式。

1.首次访问/admin/findAll,gateway会调用到8001端口下的user模块,如下图

 首次访问/course/findAll,gateway会调用到8002端口下的course模块,如下图

2.POST访问/login,并且提供相关参数(数据库存在用户111和加密过的密码111),得到token,如下图:

这时,redis数据库就有了用户名为 user的数据 

3.复制刚才返回给前端的token,在Authorization的Type中,选择Bearer Token,粘贴上刚才的Token,点击Send发送

 5.再次请求8002端口下的source模块

 再次请求8001端口下的user模块

6.当token过期后,redis中的异常时间还没到,则会返回给前端一个新的token,拿着新token继续请求即可。

7.当token过期并且redis的异常时间也过了之后,用户就需要重新登录。

8.退出则为/logout 。

  同时,redis数据库中用户名为user的key值也被删除掉。

至此,结束!祝大家520快乐!!!

推荐链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: 

发表评论

返回顶部暗黑模式