(微服务多模块)Springboot+Security+Redis+JWT 仅需一招
小编在开发阶段发现,在现在众多文章中的教程里,虽然有许许多多的Springboot+Security+Redis+JWT,但是那些教程基本的运行环境都在单个项目单个模块中进行开发和测试的,这使得小编在实际的开发过程中,不能Ctrl+C and Ctrl+V直接完全解决这个登录认证的事情。故有这篇文章.....
小编在开发阶段发现,在现在众多文章中的教程里,虽然有许许多多的Springboot+Security+Redis+JWT,但是那些教程基本的运行环境都在单个项目单个模块中进行开发和测试的,这使得小编在实际的开发过程中,不能Ctrl+C and Ctrl+V直接完全解决这个登录认证的事情。故有这篇文章。
目录
5.1 DiyUserDetails(UserDetails)
5.2 WebSecurityConfig(WebSecurityConfigurerAdapter)
5.3 TokenAuthenticationFilter(BasicAuthenticationFilter)
5.4 TokenLoginFilter(UsernamePasswordAuthenticationFilter)
5.5 TokenOncePerRequestFilter(OncePerRequestFilter)
5.6 LoginAuthenticationEntryPoint(AuthenticationEntryPoint)
5.7 LoginInFailHandler(AuthenticationFailureHandler)
5.8 LoginInSuccessHandler(AuthenticationSuccessHandler)
5.9 LogOutSuccessHandler(LogoutSuccessHandler)
5.10 NothingAccessDeniedHandler(AccessDeniedHandler)
1.项目结构
涉及的模块有common(Redis配置文件、Redis工具、Token工具、返回信息的工具;即如下文件RedisConfig、RedisUtil、TokenUtil、ResponseUtil)、model(user的实体类)、service(主要利用MySQL查询用户数据)、spring_security(多个配置文件,配置security的)。
下面小编将全部一一介绍并且源码展示出来。
2.Common模块
pom.xml
<!--security安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--springboot_redis缓存框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
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<String,Object> redisTemplate(){
RedisTemplate<String,Object> 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<Object> serializer=new Jackson2JsonRedisSerializer<Object>(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<Object, Object> data = new HashMap<Object, Object>();
public ResponseUtil(int code, String message) {
this.code=code;
this.message=message;
}
public ResponseUtil(int code, String message, Map<Object, Object> 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));
}
}
3.model模块
3.1 User
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
//实体:用户
@TableName("user")
public class User implements Serializable {
//用户id
@TableId(type = IdType.AUTO)
private Long id;
//用户名
private String username;
//密码
private String password;
//身份
private String identity;
}
4.service模块
在这个模块下启动类需要开启SpringBoot:@SpringBootApplication
pom.xml
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.Lino_white</groupId>
<artifactId>model</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.Lino_white</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.Lino_white</groupId>
<artifactId>spring_security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
4.1 UserMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.Lino_white.model.User;
import org.apache.ibatis.annotations.Select;
public interface UserMapper extends BaseMapper<User> {
@Select("select * from user where username=#{username}")
User findUserByName(String username);
}
4.2 重点说明
这里采用的是SpringBoot的方式,所以相关的Service跟ServiceImpl省略,主要讲解多模块下的Security怎么运行登录跟认证。
下面的4.3就是多模块的关键文件,在Service模块下继承了UserDetailsService
4.3 MyUserDetailService
import com.Lino_white.model.User;
import com.Lino_white.service.UserService;
import com.Lino_white.spring_security.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.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 从数据库读取用户信息(用户名,密码,身份)进行身份认证
*/
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("********开始loadUserByUsername********");
User user = userService.findUserByName(username);
System.out.println("浏览器的username:"+username);
System.out.println("数据库的username:"+user.getUsername());
if (user==null) throw new UsernameNotFoundException(username);
//根据当前用户名查询用户权限
List<String> 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;
}
}
5.spring_security模块
5.1 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<String> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> 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.2 WebSecurityConfig(WebSecurityConfigurerAdapter)
import com.Lino_white.spring_security.TokenAuthenticationFilter;
import com.Lino_white.spring_security.TokenLoginFilter;
import com.Lino_white.spring_security.TokenOncePerRequestFilter;
import com.Lino_white.spring_security.*;
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;
@Configuration
@EnableWebSecurity //开启Security功能
@EnableGlobalMethodSecurity(prePostEnabled = true) //启动方法级别的权限认证
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myUserDetailService;
@Bean
//配置密码加密器
public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
//配置哪些请求不拦截
@Override
public void configure(WebSecurity web) throws Exception {
// web.ignoring().antMatchers("/index","/api/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
//配置安全策略
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/index").authenticated()
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll()
.and()
.addFilterBefore(new TokenOncePerRequestFilter(myUserDetailService), TokenLoginFilter.class)
//登录后,访问没有权限处理类
.exceptionHandling().accessDeniedHandler(new NothingAccessDeniedHandler())
//匿名访问,没有权限的处理类
.authenticationEntryPoint(new LoginAuthenticationEntryPoint())
//.anyRequest().authenticated()
.and()
.addFilter(new TokenLoginFilter(authenticationManager()))
.addFilter(new TokenAuthenticationFilter(authenticationManager()))
.formLogin()
.successHandler(new LoginInSuccessHandler())
.failureHandler(new LoginInFailHandler())
// 不需要session
.and()
.logout()
.logoutSuccessHandler(new LogOutSuccessHandler())
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
;
}
}
5.3 TokenAuthenticationFilter(BasicAuthenticationFilter)
import com.Lino_white.common.TokenUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(TokenUtil.TOKEN_HEAD);
if (token==null || !token.startsWith(TokenUtil.TOKEN_PREFIX)){
// 如果请求头中没有Authorization信息则直接放行了
chain.doFilter(request,response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
String usernameFromToken = new TokenUtil().getUsernameFromToken(token.replace(TokenUtil.TOKEN_PREFIX,""));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(usernameFromToken, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
super.doFilterInternal(request, response, chain);
}
}
5.4 TokenLoginFilter(UsernamePasswordAuthenticationFilter)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.Lino_white.model.User;
import com.Lino_white.spring_security.LoginInSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* 登录过滤器,继承UsernamePasswordAuthenticationFilter
* 对用户名密码进行登录校验,或对用户密码进行私钥解密等操作
*
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public TokenLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
this.setPostOnly(false);
// 认证路径 - 发送什么请求,就会进行认证
this.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/login", "POST")
);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("进入UsernamePasswordAuthenticationFilter:username="+username+" password="+password);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// usernamePasswordAuthenticationToken.setDetails(authenticationDetailsSource.buildDetails(request));
this.setDetails(request, usernamePasswordAuthenticationToken);
return authenticationManager.authenticate(usernamePasswordAuthenticationToken);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("Filter认证成功------Start");
User user = (User) authResult.getPrincipal();
LoginInSuccessHandler loginInSuccessHandler = new LoginInSuccessHandler();
loginInSuccessHandler.onAuthenticationSuccess(request, response, chain, authResult);
System.out.println("Filter认证成功------End");
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
System.out.println("Filter认证失败------");
super.unsuccessfulAuthentication(request, response, failed);
}
}
5.5 TokenOncePerRequestFilter(OncePerRequestFilter)
import com.Lino_white.common.RedisUtil;
import com.Lino_white.common.ResponseUtil;
import com.Lino_white.common.TokenUtil;
import com.Lino_white.spring_security.DiyUserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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.Date;
/**
* 过滤器,在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中
* @author Lino_white
*/
@Component
public class TokenOncePerRequestFilter extends OncePerRequestFilter {
@Autowired
UserDetailsService userDetailsService;
StringRedisTemplate stringRedisTemplate=RedisUtil.stringRedisTemplateStatic;
public TokenOncePerRequestFilter(UserDetailsService myUserDetailService) {
userDetailsService=myUserDetailService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String authHeader = request.getHeader(TokenUtil.TOKEN_HEAD);
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;
Claims claims;
DiyUserDetails userDetails;
try {
claims = new TokenUtil().parseJwt(authToken);
username = claims.getSubject();
} catch (ExpiredJwtException e) {
//token过期
claims = e.getClaims();
username = claims.getSubject();
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 || token=redis || redis <now 则token失效,跳转登录
* token<redis
* */
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
userDetails = (DiyUserDetails) userDetailsService.loadUserByUsername(username);
String token = TokenUtil.createToken(userDetails,date);
System.out.println("—————————————————start—————————————————————");
System.out.println("token:"+token);
RedisUtil.redis_SaveTokenInfo(token,username);
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;
}
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
System.out.println("OncePerRequestFilter 中的username :" +username);
userDetails = (DiyUserDetails) userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
5.6 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.7 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.8 LoginInSuccessHandler(AuthenticationSuccessHandler)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.Lino_white.common.RedisUtil;
import com.Lino_white.common.ResponseUtil;
import com.Lino_white.common.TokenUtil;
import com.Lino_white.model.User;
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;
/**
* 登录成功处理类,登录成功后会调用里面的方法
* @author Lino_white
*/
@Component
public class LoginInSuccessHandler implements AuthenticationSuccessHandler {
/**
* 用户通过TokenLoginFilter(UsernamePasswordAuthenticationFilter)后,
* 验证成功到这里进行 token创建,并将其存入redis
*
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
User user =(User) authentication.getPrincipal();
String token = TokenUtil.createToken(user);
//redis缓存token
RedisUtil.redis_SaveTokenInfo(token,user.getUsername());
//写入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<String,Object> map=new HashMap<String,Object>(4);
map.put("code",HttpServletResponse.SC_OK);
map.put("message","登录成功");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}catch (Exception e){
e.printStackTrace();
}
response.setStatus(200);
chain.doFilter(request, response);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
}
}
5.9 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.10 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,"当前您没有该权限");
}
}
6.测试
这里是使用postman工具进行测试的,在这里之前已经在数据库有用户名跟密码都为user的数据,并且密码已是加密形式。
1.首次访问index,如下图
2.访问/login,并且提供相关参数,如下图:
这时,redis数据库就有了用户名为 user的数据
3.在返回Headers信息中找到token,复制除了Bearer+空格以外的东西,如下图的蓝色底token
4.将请求方式改成Get,在Authorization的Type中,选择Bearer Token,粘贴上刚才的Token,点击Send发送
5.就会显示可以正常访问index页面了
6.退出则为/logout ,带上参数username即可,请求方式为POST。
同时,redis数据库中用户名为user的key值也被删除掉。
更多推荐
所有评论(0)