赞
踩
主要是自己想学习一下,也分享给大家,主要参考 链接: link
在这里我不会去介绍SpringSecurity 和JWT,直接上代码的形式,不了解的可以去了解一下
已经更新了,代码已完整、可以正常运行
有问题可以留言,对你有帮助请点个赞,让更多人学习到。
1、先整合security:
使用自己数据库的用户,来登录认证(说一下我没有用到角色权限)
2、整合jwt:
能登录认证了,那就需要生成token
3、过滤器Filter:
每次调用接口时认证token合法性
4、jwt刷新机制:
我用的是缓存刷新机制,我这里弄的比较简单用的是SpringBoot自带的缓存
5、扩展:
比如token生成使用的秘钥可以使用RAS私钥公钥,提高安全性。缓存可以使用redis
在这里我就不使用了。
主要的依赖是 spring-boot-starter-web、spring-boot-starter-security、jjwt
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> <exclusion> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> </exclusion> <exclusion> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> </exclusion> <exclusion> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> </exclusion> </exclusions> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 常用工具类 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <!-- json工具 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.7</version> </dependency> <!-- log4日志 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> <version>2.7.9</version> </dependency> <!-- JJWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
直接建立一个简单的用户表
用户id | 用户名 | 用户名 |
---|---|---|
userid | username | password |
给用户表创建实体类,并实现UserDetails。
UserDetails是security加载用户信息的接口,可以用来身份认证和授权
public class UserDetailsInfo implements UserDetails { private Integer userid; private String username; private String password; public Integer getUserid() { return userid; } public void setUserid(Integer userid) { this.userid = userid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username == null ? null : username.trim(); } public String getPassword() { return password; } public void setPassword(String password) { this.password = password == null ? null : password.trim(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } //判断用户是否过期 @Override public boolean isAccountNonExpired() { return true; } /* *判断用户是否被锁定 * 一般用于多次登录失败。 */ @Override public boolean isAccountNonLocked() { return true; } /* *用于判断用户的凭证(密码)是否过期 */ @Override public boolean isCredentialsNonExpired() { return true; } /* *判断用户是否被禁用 */ @Override public boolean isEnabled() { return true; } }
用于访问用户表
1、UserDetailsMapper
@Repository
public interface UserDetailsMapper {
UserDetailsInfo getUserInfoByUsername(String username);
}
2、UserDetailsMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.stc.login.mapper.UserDetailsMapper"> <resultMap id="BaseResultMap" type="com.stc.login.model.UserDetailsInfo"> <id column="UserId" jdbcType="INTEGER" property="userid" /> <result column="Username" jdbcType="NVARCHAR" property="username" /> <result column="Password" jdbcType="NVARCHAR" property="password" /> </resultMap> <sql id="Base_Column_List"> UserId, Username, Password </sql> <select id="getUserInfoByUsername" parameterType="java.lang.String" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from STC_EmployeePassWord where Username = #{username,jdbcType=NVARCHAR} </select> </mapper>
UserDetailsServiceImpl 是一个实现了 UserDetailsService 接口的类,它可以用来自定义从数据库中获取用户信息的逻辑。
UserDetailsService 接口只有一个方法 loadUserByUsername ,它接收一个用户名作为参数,返回一个 UserDetails 对象,表示用户的核心信息,包括用户名、密码、权限等。
我这里密码是明码,所以把数据库密码取出来在加密的,不然你的安全机制使用了加密,数据库密码又不加密会认证失败的(passwordEncoder.encode)
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserDetailsMapper userDetailsMapper; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetailsInfo userDetailsInfo = userDetailsMapper.getUserInfoByUsername(username); userDetailsInfo.setPassword( passwordEncoder.encode(userDetailsInfo.getPassword())); if(userDetailsInfo == null){ throw new UsernameNotFoundException("用戶不存在"); } return userDetailsInfo; } }
SecurityConfig 是一个配置类,它可以用来自定义 Spring Security 的配置,包括认证、授权、跨域、异常处理等。
注意:spring boot2.7以上版本不需要继承WebSecurityConfigurerAdapter 类,直接使用@Bean注解
注意2:我这里就直接把完整的代码贴上来,已经加上了jwtAuthenticationFilter 和异常处理
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JWTAuthenticationFilter jwtAuthenticationFilter; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; /** * 密码明文加密方式配置 *默认加密方式 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 获取AuthenticationManager(认证管理器),登录时认证使用 * 默认认证 * @param authenticationConfiguration * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable(); // 基于 token,不需要 csrf http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 基于 token,不需要 session // 下面开始设置权限 http.authorizeRequests(authorize -> authorize .antMatchers("/stc/login/login.action").permitAll()不需要进行身份验证的接口 .anyRequest().authenticated()//除上面外的所有请求全部需要 鉴权认证 ); //定义filter的先后顺序,保证 jwtFilter比用户验证的过滤器先执行 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); //自定义异常捕获机制 http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint).accessDeniedHandler(myAccessDeniedHandler); return http.build(); } }
1、LoginAction
@RestController /* @RestController 是 @ResponseBody 和 @Controller 的组合注解 */
@RequestMapping("/stc/login")
public class LoginAction {
@Autowired
private LoginService loginService;
@RequestMapping(value = {"login.action"}, method = RequestMethod.POST)
public Map<String, String> login(@RequestBody JSONObject jsonObject) {
return loginService.login(jsonObject);
}
}
2、LoginService
public interface LoginService {
public Map<String, String> login(JSONObject jsonObject);
}
3、LoginServiceImp
@Service public class LoginServiceImp implements LoginService { private static final Logger log = LoggerFactory.getLogger(LoginServiceImp.class); @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserTokenCacheService userTokenCacheService; @Autowired private AuthenticationManager authenticationManager; @Override public Map<String, String> login(JSONObject jsonObject) { Map<String, String> loginMap = new HashMap<>(); //封装 Authentication UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jsonObject.get("username"), jsonObject.get("password")); //认证用户 Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (authenticate == null) { throw new RunException("认证失败!"); } //获取认证用户信息 UserDetailsInfo userDetailsInfo = (UserDetailsInfo) authenticate.getPrincipal(); //认证通过,生成jwt String token = jwtTokenUtil.generateToken(userDetailsInfo); loginMap.put("userid", String.valueOf(userDetailsInfo.getUserid())); loginMap.put("username", userDetailsInfo.getUsername()); loginMap.put("token", token); //存入缓存中 userTokenCacheService.userTokenCachePut(loginMap); return loginMap; } }
我已经把完整的代码贴出来了,包含了生成token,存入缓存,把生成好的token返回给客户端端,其他的接口就直接token认证
@Component public class JwtTokenUtil { private static final Logger log = LoggerFactory.getLogger(JwtTokenUtil.class); public static final String TOKEN_HEADER = "x-access-token"; //token请求头 public static final String TOKEN_PREFIX = "Bearer";//token前缀 private static final String ISSUER = "STC"; //发行方 private static final String SUBJECT = "OMM"; //签名主题 private static final String AUDIENCE = "OMM-VUE"; //接收方 private String ROLE_CLAIMS = "role"; //角色权限声明 private String secret = "secret"; //jwt加解密使用的密钥; private int expiration = 60; //jwt过期时间 秒级别 private static final String CLAIM_KEY_USERNAME = "username";//登录的用户号 private static final String CLAIM_KEY_CREATED = "created";// jwt创建时间 /** * 生成token的过期时间 */ private Date generateExpirationDate() { //毫秒级 return new Date(System.currentTimeMillis() + expiration * 1000); } /** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());//用户名 claims.put(CLAIM_KEY_CREATED, new Date());//创建日期 return Jwts.builder() .setIssuer(ISSUER)//发行方 .setSubject(SUBJECT)//主题 .setAudience(AUDIENCE)//接收方 .setClaims(claims)//其他信息,主要是用户信息 .setIssuedAt(new Date())//签发日期 .setNotBefore(new Date())//生效日期 .setExpiration(generateExpirationDate())//失效时间 .signWith(SignatureAlgorithm.HS512, secret)//生成算法 .compact(); } /** * 从token中获取JWT中的有效载荷 */ public Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { // 捕获过期异常 log.info("token:" + token); log.info("token已经过期:" + e.getMessage()); claims = e.getClaims(); } catch (Exception e) { log.info("token:" + token); log.info("JWT格式验证失败:" + e.getMessage()); } return claims; } /** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.get("username").toString(); } /** * 从token中获取过期时间 */ public Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 判断token是否已经失效 */ public boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } }
JWTAuthenticationFilter 是一个自定义的过滤器类,它继承了 OncePerRequestFilter 类,用于实现 JWT 的验证机制。
JWTAuthenticationFilter 需要重写 doFilterInternal 方法,用于检查请求头中是否有 Authorization 字段,并验证 JWT 的正确性,然后将认证信息设置到 SecurityContext 中
JWTAuthenticationFilter 需要在 SecurityConfig 中通过 addFilterBefore 方法添加到过滤器链中,并指定在 UsernamePasswordAuthenticationFilter 之前执行。
@Component public class JWTAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserTokenCacheService userTokenCacheService; protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //需要认证token的接口才进入 if(!notNeedFilter(request)){ // 获取 token String token = request.getHeader(jwtTokenUtil.TOKEN_HEADER); if (StringUtils.hasText(token)) { // 解析token,获取 username String username = jwtTokenUtil.getUserNameFromToken(token); //判断是否有该username 的缓存,(登录存入缓存,注销清除缓存) userTokenCacheInfo userTokenCacheInfo = userTokenCacheService.getUserTokenCacheById(username); if (userTokenCacheInfo != null) { //判断存储的token跟传入的token是否一致, String CToken = userTokenCacheInfo.getToken(); if (CToken.equals(token)) { //判断token是否过期 if (jwtTokenUtil.isTokenExpired(token)) { //判断是否超过最大失效时间 if (userTokenCacheInfo.getMaxExpirationDate().before(new Date())) { ResponseProcessing(request, response, "exceeding-maximum-failure-time", null); } //是否超过设定时间没有操作 else if (DateUtils.addMinutes(userTokenCacheInfo.getOldOperationDate(), userTokenCacheInfo.getInactiveMinute()).before(new Date())) { ResponseProcessing(request, response, "exceeding-inactive-time", null); } //满足条件 活跃用户,又没有超过最大失效时间,从新生成token给客户端 else { UserDetailsInfo userDetailsInfo = new UserDetailsInfo(); userDetailsInfo.setUsername(username); //生成jwt String newToken = jwtTokenUtil.generateToken(userDetailsInfo); //更新缓存 userTokenCacheService.userTokenCacheUpdate(newToken, username); ResponseProcessing(request, response, "renovate-token", newToken); } } else { //用户是否授权 if (SecurityContextHolder.getContext().getAuthentication() == null) { //将User 封装到 securityContextHolder。 封装到securityContextHolder以后,其他过滤器就不会在拦截 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userTokenCacheInfo, null, null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } //认证成功,更改缓存里面的调用接口时间,主要用于判断是否活跃 userTokenCacheService.userTokenCacheOldOperationDateUpdate(userTokenCacheInfo, username); } } } } } filterChain.doFilter(request, response); } //直接返回给客户端 private void ResponseProcessing(HttpServletRequest request, HttpServletResponse response, String type, String token) throws IOException { Map<String, Object> map = new HashMap<>(); map.put("uri", request.getRequestURI()); if (type.equals("exceeding-maximum-failure-time")) { map.put("msg", "exceeding-maximum-failure-time"); } else if (type.equals("exceeding-inactive-time")) { map.put("msg", "exceeding-inactive-time"); } else if (type.equals("renovate-token")) { map.put("msg", "renovate-token"); map.put("token", token); } else { map.put("msg", "身份认证失败"); } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); String resBody = JSON.toJSONString(map); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } /** * 判断是否可以通过过滤 */ private boolean notNeedFilter(HttpServletRequest request) { //登录接口 if ((request.getContextPath() + "/stc/login/login.action").equals(request.getRequestURI())) { return true; } return false; } }
我已经把完整的代码贴出,使用到了缓存
我这里的token 使用的是缓存刷新token机制
第一点:如果超过最大的失效日期,需要客户端重新输入用户名和密码认证,发放新的token
第二点:我设置是30分钟不活跃,需要客户端重新输入用户名和密码认证,发放新的token
第三点:不执行上面两点,token过期直接发放新的token,然后客户端获取token重新发送请求。
包含登录成功调用缓存,更新调用缓存,更新活跃时间调用缓存,注销删除缓存,
Spring boot 自带缓存可以去了解一下。
1、userTokenCacheInfo
public class userTokenCacheInfo { private String token; private String username; private Date loginDate;//登录时间 private Date notBeforeDate; //token生效时间 private Date expirationDate; //token过期时间 private int refreshCount = 0; //token的刷新次数 private Date maxExpirationDate;//token最大的失效时间 private Date OldOperationDate;//上一次调用接口的操作时间 private String equipmentIP;//设备ip private int InactiveMinute = 30; //默认30分钟,用于如果30分钟不操作,token失效。 public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Date getLoginDate() { return loginDate; } public void setLoginDate(Date loginDate) { this.loginDate = loginDate; } public Date getNotBeforeDate() { return notBeforeDate; } public void setNotBeforeDate(Date notBeforeDate) { this.notBeforeDate = notBeforeDate; } public Date getExpirationDate() { return expirationDate; } public void setExpirationDate(Date expirationDate) { this.expirationDate = expirationDate; } public int getRefreshCount() { return refreshCount; } public void setRefreshCount(int refreshCount) { this.refreshCount = refreshCount; } public Date getMaxExpirationDate() { return maxExpirationDate; } public void setMaxExpirationDate(Date maxExpirationDate) { this.maxExpirationDate = maxExpirationDate; } public Date getOldOperationDate() { return OldOperationDate; } public void setOldOperationDate(Date oldOperationDate) { OldOperationDate = oldOperationDate; } public String getEquipmentIP() { return equipmentIP; } public void setEquipmentIP(String equipmentIP) { this.equipmentIP = equipmentIP; } public int getInactiveMinute() { return InactiveMinute; } public void setInactiveMinute(int inactiveMinute) { InactiveMinute = inactiveMinute; } }
2、UserTokenCacheService
public interface UserTokenCacheService {
public void userTokenCachePut(Map<String, String> loginMap);
public userTokenCacheInfo getUserTokenCacheById(Object key);
public void userTokenCacheUpdate(String token, Object key);
public void userTokenCacheOldOperationDateUpdate(userTokenCacheInfo userTokenCacheInfo, Object key);
public void userTokenCacheEvict(Object key);
}
3、UserTokenCacheServiceImpl
@Service public class UserTokenCacheServiceImpl implements UserTokenCacheService { // 注入缓存管理器 @Autowired private CacheManager cacheManager; @Autowired private JwtTokenUtil jwtTokenUtil; // 定义userTokenCache()方法,用于获取或创建"userTokenCache"缓存对象 //用于Token缓存 private Cache userTokenCache() { // 获取或创建名为"userCache"的缓存对象 Cache cache = cacheManager.getCache("userTokenCache"); return cache; } // 从缓存中获取用户数据 @Override public userTokenCacheInfo getUserTokenCacheById(Object key) { // 获取"userCache"缓存对象 Cache cache = userTokenCache(); // 从缓存中获取用户数据 userTokenCacheInfo userTokenCacheInfo = cache.get(key, userTokenCacheInfo.class); return userTokenCacheInfo; } //初始化放入缓存中 @Override public void userTokenCachePut(Map<String, String> loginMap) { // 获取"userCache"缓存对象 Cache cache = userTokenCache(); // 从缓存中获取用户数据 userTokenCacheInfo userTokenCacheInfo = getUserTokenCacheById(loginMap.get("username")); if (userTokenCacheInfo == null) { userTokenCacheInfo = new userTokenCacheInfo(); } userTokenCacheInfo.setUsername(loginMap.get("username")); userTokenCacheInfo.setToken(loginMap.get("token")); //获取token的信息 Claims claims = jwtTokenUtil.getClaimsFromToken(loginMap.get("token")); userTokenCacheInfo.setLoginDate(claims.getNotBefore());//登录日期 userTokenCacheInfo.setNotBeforeDate(claims.getNotBefore());//生效日期 userTokenCacheInfo.setExpirationDate(claims.getExpiration());//失效日期 userTokenCacheInfo.setRefreshCount(0);//刷新次数 userTokenCacheInfo.setOldOperationDate(claims.getNotBefore());//上一次调用接口的操作时间 userTokenCacheInfo.setMaxExpirationDate(DateUtils.addHours(claims.getNotBefore(), 4));//最大失效时间 // 将用户数据放入缓存中 cache.put(loginMap.get("username"), userTokenCacheInfo); } //更新缓存token @Override public void userTokenCacheUpdate(String token, Object key) { // 从缓存中获取用户数据 userTokenCacheInfo userTokenCacheInfo = getUserTokenCacheById(key); //获取token的信息 Claims claims = jwtTokenUtil.getClaimsFromToken(token); userTokenCacheInfo.setToken(token); userTokenCacheInfo.setNotBeforeDate(claims.getNotBefore());//生效日期 userTokenCacheInfo.setExpirationDate(claims.getExpiration());//失效日期 userTokenCacheInfo.setRefreshCount(userTokenCacheInfo.getRefreshCount() + 1);//刷新次数 userTokenCacheInfo.setOldOperationDate(new Date());//上一次调用接口的操作时间 // 获取"userCache"缓存对象 Cache cache = userTokenCache(); // 将用户数据放入缓存中 cache.put(key, userTokenCacheInfo); } //更新缓存 调用接口的操作时间 @Override public void userTokenCacheOldOperationDateUpdate(userTokenCacheInfo userTokenCacheInfo, Object key) { userTokenCacheInfo.setOldOperationDate(new Date());//上一次调用接口的操作时间 // 获取"userCache"缓存对象 Cache cache = userTokenCache(); // 将用户数据放入缓存中 cache.put(key, userTokenCacheInfo); } //删除缓存 @Override public void userTokenCacheEvict(Object key) { // 获取"userCache"缓存对象 Cache cache = userTokenCache(); cache.evict(key); } }
1、MyAuthenticationEntryPoint
@Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { Map<String, Object> map = new HashMap<>(); map.put("uri", httpServletRequest.getRequestURI()); map.put("msg", "身份认证失败");//身份认证失败 httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(map); PrintWriter printWriter = httpServletResponse.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
2、MyAccessDeniedHandler
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { Map<String, Object> map = new HashMap<>(); map.put("uri",httpServletRequest.getRequestURI()); map.put("msg","鉴权失败");//没有足够的权限访问该资源 httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(map); PrintWriter printWriter = httpServletResponse.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
1、登录接口测试获取token
2、缓存机制测试 这里我就直接拿我的客户端测试了
没有token、或者跟缓存的token不一样、直接访问,进入异常类返回身份认证失败
3、token过期了,重新生成token
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。