Spring Security配置(包含WebSecurityConfigurerAdapter过时问题)
Spring Security配置(包含WebSecurityConfigurerAdapter过时问题)
- pom.xml文件引入以下依赖
<!--spring security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.7.0</version> </dependency> <!--JWT--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!-- 排除lettuce包,使用jedis代替--> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> <version>2.7.0</version> </dependency> <!--jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.7.1</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.9</version> </dependency>
- application.yml文件配置
# Tomcat server: port: 8088 tomcat: uri-encoding: UTF-8 connection-timeout: 5000ms threads: max: 1000 min-spare: 30 servlet: context-path: /gremlin encoding: charset: UTF-8 # spring配置 spring: mvc: pathmatch: matching-strategy: ant_path_matcher datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/forum?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root redis: database: 0 # Redis数据库索引(默认为0) host: 127.0.0.1 # Redis服务器地址 port: 6379 # Redis服务器连接端口 jedis: pool: max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8 max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 max-idle: 8 # 连接池中的最大空闲连接 默认 8 min-idle: 0 # 连接池中的最小空闲连接 默认 0
- security配置 (编写SecurityConfig配置文件(WebSecurityConfigurerAdapter过时代替配置在文末))
package com.gremlin.config; import com.gremlin.common.utils.RedisUtils; import com.gremlin.log.service.LogService; import com.gremlin.power.mapper.PowerManagerMapper; import com.gremlin.power.service.PowerManagerService; import com.gremlin.security.fifter.JwtAuthenticationTokenFilter; import com.gremlin.security.fifter.TokenLoginFilter; import com.gremlin.security.handler.AjaxAccessDeniedHandler; import com.gremlin.security.handler.AjaxAuthenticationEntryPoint; import com.gremlin.security.handler.AjaxLogoutSuccessHandler; import com.gremlin.security.service.MyUserDetailsServiceImpl; import com.gremlin.security.strategy.CustomizeSessionInformationExpiredStrategy; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * Description: security配置类 * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 注销成功返回的 JSON 格式数据给前端 */ @Autowired private AjaxLogoutSuccessHandler logoutSuccessHandler; /** * 无权访问 JSON 格式的数据 */ @Autowired private AjaxAccessDeniedHandler ajaxAccessDeniedHandler; @Autowired private AjaxAuthenticationEntryPoint authenticationEntryPoint; @Autowired private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy; @Autowired private JwtAuthenticationTokenFilter tokenAuthenticationFilter; @Autowired private RedisUtils redisUtils; @Autowired private LogService logService; @Autowired private PowerManagerMapper powerManagerMapper; @Autowired private MyUserDetailsServiceImpl userDetailsService; @Autowired private PowerManagerService powerManagerService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf http.cors().and().csrf().disable(); http.httpBasic() .authenticationEntryPoint(authenticationEntryPoint) .and() .authorizeRequests() //自定义放行接口 .antMatchers( "/swagger**/**", "/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/v3/**" ).permitAll() .anyRequest() .authenticated() .and().logout().logoutUrl("/logout") //登出处理 .logoutSuccessHandler(logoutSuccessHandler) //添加关于自定义的认证过滤器和自定义的授权过滤器 .and() .logout().permitAll()//注销行为任意访问 //会话管理 .and().sessionManagement() //同一账号同时登录最大用户数 .maximumSessions(1) //会话信息过期策略会话信息过期策略(账号被挤下线) .expiredSessionStrategy(sessionInformationExpiredStrategy); //自定义权限拒绝处理类 // 无权访问 JSON 格式的数据 http.exceptionHandling().accessDeniedHandler(ajaxAccessDeniedHandler); // 登录验证 http.addFilter(new TokenLoginFilter(authenticationManager(),redisUtils,logService,powerManagerMapper)).httpBasic(); // JWT Filter http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode"); } @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
- 编写UserDetailsService的实现类
package com.gremlin.security.service; import com.gremlin.security.mapper.UserMapper; import com.gremlin.security.vo.MyUserDetails; import com.gremlin.security.vo.User; import com.gremlin.security.vo.req.ReqUser; import org.springframework.beans.factory.annotation.Autowired; 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; /** * Description: 自定义的Service,实现于springSecurity框架的UserDetailsService * @author: gremlin * Date: 2022/6/10 13:02 * @version: 1.0.0 */ @Service public class MyUserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String loginUsername) throws UsernameNotFoundException { ReqUser reqUser = new ReqUser(); reqUser.setLoginUsername(loginUsername); // 通过账户查询账户信息 User userInfo = userMapper.getUserInfo(reqUser); if (null == userInfo){ throw new UsernameNotFoundException("该用户不存在或被禁用请联系管理员"); } String password = userInfo.getPassword(); //找到该账号下面的权限 return new MyUserDetails(loginUsername,password); } }
- 编写token登录过滤器
package com.gremlin.security.fifter; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.gremlin.account.vo.req.ReqAccount; import com.gremlin.common.resp.ResponseResult; import com.gremlin.common.resp.ResultEnum; import com.gremlin.common.utils.AccessAddressUtils; import com.gremlin.common.utils.JwtTokenUtils; import com.gremlin.common.utils.RedisUtils; import com.gremlin.log.service.LogService; import com.gremlin.power.mapper.PowerManagerMapper; import com.gremlin.power.vo.resp.RespRole; import com.gremlin.security.vo.MyUserDetails; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; /** * Description: token登录过滤器 * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Slf4j public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private final RedisUtils redisUtil; private final AuthenticationManager authenticationManager; private final LogService logService; private final PowerManagerMapper powerManagerMapper; public TokenLoginFilter(AuthenticationManager authenticationManager, RedisUtils redisUtil, LogService logService,PowerManagerMapper powerManagerMapper) { this.authenticationManager = authenticationManager; this.redisUtil = redisUtil; this.logService = logService; this.powerManagerMapper = powerManagerMapper; } /** * 通过前端传来的json数据(用户名 密码),解析成我们的java对象,最终得到用户名和密码去进行认证 */ @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { System.out.println(2); //只允许post请求 if ("POST".equals(req.getMethod())) { // 获取请求体 String body = getBody(req); // 转换为json格式 JSONObject jsonObject = JSON.parseObject(body); String username = jsonObject.getString("loginUsername"); String password = jsonObject.getString("password"); // 获取ip地址 String ip = AccessAddressUtils.getIpAddress(req); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); //插入用户登录成功日志 logService.insertLog(ip, "1", "登入", username); return authenticationManager.authenticate(authRequest); } return null; } /** * 若认证成功,根据用户名生成token返回给前端。并以用户名为key,权限列表为value的形式存入redis缓存中 */ @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException { System.out.println(4); Map<String, Object> map = new HashMap<>(); // 获取IP地址 String ip = AccessAddressUtils.getIpAddress(req); map.put("ip", ip); // 获取当前用户信息 MyUserDetails userDetails = (MyUserDetails) auth.getPrincipal(); // 通过jwt生成jwtToken String jwtToken = JwtTokenUtils.generateToken(userDetails.getUsername(),map); // 将token赋值到用户对象类 userDetails.setToken(jwtToken); // 通过用户名称查询用户的角色 ReqAccount reqAccount = new ReqAccount(); reqAccount.setLoginUsername(userDetails.getUsername()); RespRole respRole = new RespRole(); respRole = powerManagerMapper.queryRoleByUsername(reqAccount); map.put("juese",respRole); //获取请求的ip地址 redisUtil.setTokenRefresh(userDetails.getUsername(), userDetails.getToken(), ip); log.info("用户{}登录成功,信息已保存至redis", userDetails.getUsername()); res.setHeader("Content-type", "application/json;charset=UTF-8"); map.put("token", jwtToken); redisUtil.set(userDetails.getUsername()+"token",jwtToken); res.getWriter().write(JSON.toJSONString(ResponseResult.success(map, ResultEnum.SUCCESS.getCode(), ResultEnum.USER_LOGIN_SUCCESS.getMessage()))); } /** * 登录失败 若认证失败,则返回错误信息给前端 */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { response.setHeader("Content-type", "application/json;charset=UTF-8"); if (e instanceof UsernameNotFoundException) { response.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.USER_NOT_FIND.getMessage(), ResultEnum.USER_NOT_FIND.getCode()))); } else if (e instanceof BadCredentialsException) { response.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.USER_LOGIN_FAILED.getMessage(), ResultEnum.USER_LOGIN_FAILED.getCode()))); } else if (e instanceof LockedException) { response.getWriter().write(JSON.toJSONString(ResponseResult.failed("用户已被锁定", 207))); }else { response.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.USER_LOGIN_FAILED.getMessage(), ResultEnum.USER_LOGIN_FAILED.getCode()))); } } /** * 获取请求Body */ 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, StandardCharsets.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(); } }
- 编写token认证过滤器
package com.gremlin.security.fifter; import com.alibaba.fastjson2.JSON; import com.gremlin.common.resp.ResponseResult; import com.gremlin.common.resp.ResultEnum; import com.gremlin.common.utils.CollectionUtil; import com.gremlin.common.utils.DateUtil; import com.gremlin.common.utils.JwtTokenUtils; import com.gremlin.common.utils.RedisUtils; import com.gremlin.security.service.MyUserDetailsServiceImpl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * Description: token认证过滤器 * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Component @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private MyUserDetailsServiceImpl userDetailsService; @Autowired private RedisUtils redisUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { System.out.println(1); String authToken = request.getHeader("token"); response.setHeader("Content-type", "application/json;charset=UTF-8"); if (null != authToken) { //判断token是否有效 if (!JwtTokenUtils.isTokenExpired(authToken)) { response.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.LOGIN_IS_OVERDUE.getMessage(), ResultEnum.LOGIN_IS_OVERDUE.getCode()))); } // 通过token获取用户名 String username = JwtTokenUtils.parseToken(authToken); if (StringUtils.isBlank(username)) { return; } String ip = CollectionUtil.getMapValue(JwtTokenUtils.getClaims(authToken), "ip"); //判断redis是否有保存 if (redisUtil.hasKey(username)) { //有效时间 String expirationTime = redisUtil.hasGet(username, "expirationTime").toString(); if (JwtTokenUtils.isExpiration(expirationTime)) { // token失效 //当前时间超过有效时间,用户登录失效 //获得redis中用户的token刷新时效 String tokenValidTime = (String) redisUtil.getTokenValidTimeByToken(username); String currentTime = DateUtil.getTime(); //这个token已作废,加入黑名单 if (DateUtil.compareDate(currentTime, tokenValidTime) && !DateUtil.compareDate(tokenValidTime, expirationTime)) { //超过有效期,不予刷新 log.info("{}已超过有效期,不予刷新", authToken); response.getWriter().write(JSON.toJSONString(ResponseResult.success(null, ResultEnum.LOGIN_IS_OVERDUE.getCode(), ResultEnum.LOGIN_IS_OVERDUE.getMessage()))); return; } else { //仍在有效时间内,判断是否到达刷新时间,如果到达刷新时间 // (刷新时间判断方法为token时间-当前时间<=5min),刷新token否则不更新token Date tokenTime = JwtTokenUtils.getTokenTime(authToken); // 则刷新token,放入请求头中 String usernameByToken = (String) redisUtil.getUsernameByToken(username); //更新username username = usernameByToken; //更新ip ip = (String) redisUtil.getIpByToken(username); //token中的时间与当前时间做对比, if (DateUtil.compareDate(tokenTime, new Date())) { Map<String, Object> map = new HashMap<>(); map.put("ip", ip); String jwtToken = JwtTokenUtils.generateToken(usernameByToken,map); redisUtil.setTokenRefresh(username, jwtToken, ip); log.info("redis已删除旧token:{},新token:{}已更新redis", authToken, jwtToken); //更新token,为了后面 authToken = jwtToken; } redisUtil.set(username + "token", authToken, 1200); } } } else { log.info("{}redis登录信息不存在", username); response.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.LOGIN_IS_OVERDUE.getMessage(), ResultEnum.LOGIN_IS_OVERDUE.getCode()))); return; } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); response.reset(); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } }
- 编写实现UserDetails的实体类
package com.gremlin.security.vo; import io.swagger.annotations.ApiModelProperty; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; /** * Description: 自定义的实体类,实现于spring security框架的UserDetails * @author: gremlin * Date: 2022/6/10 13:06 * @version: 1.0.0 */ public class MyUserDetails implements UserDetails { @ApiModelProperty(value = "用户账户") private String username; @ApiModelProperty(value = "用户密码") private String password; @ApiModelProperty(value = "用户token值") private String token; public MyUserDetails(String username, String password) { this.username = username; this.password = password; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setToken(String token) { this.token = token; } public String getToken() { return token; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
- 编写无权访问响应类
package com.gremlin.security.handler; import com.alibaba.fastjson2.JSON; import com.gremlin.common.resp.ResponseResult; import com.gremlin.common.resp.ResultEnum; 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; /** * Description: 无权访问 * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Component public class AjaxAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8"); httpServletResponse.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.USER_NO_ACCESS.getMessage(),ResultEnum.USER_NO_ACCESS.getCode()))); } }
- 编写token认证身份验证入口点
package com.gremlin.security.handler; import com.alibaba.fastjson.JSON; import com.gremlin.common.resp.ResponseResult; import com.gremlin.common.resp.ResultEnum; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Description: 用户未登录时返回给前端的数据 身份验证入口点 * @author: gremlin * Date: 2022/6/10 17:24 * @version: 1.0.0 */ @Component public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setHeader("Content-type", "application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.USER_NEED_AUTHORITIES.getMessage(),ResultEnum.USER_NEED_AUTHORITIES.getCode()))); } }
- 编写自定义处理注销成功的类
package com.gremlin.security.handler; import com.alibaba.fastjson2.JSON; import com.gremlin.common.resp.ResponseResult; import com.gremlin.common.resp.ResultEnum; import com.gremlin.common.utils.JwtTokenUtils; import com.gremlin.common.utils.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Description: 自定义处理注销成功的类 * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Component @Slf4j public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler { @Autowired private RedisUtils redisUtil; @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException { // 获取头部信息的token String authToken = req.getHeader("token"); if (null != authToken) { String username = JwtTokenUtils.parseToken(authToken); // 清除redis里的token等其他值 redisUtil.deleteKeys(username); log.info("用户登出成功!token:{}已从redis删除", authToken); } resp.setHeader("Content-type", "application/json;charset=UTF-8"); resp.getWriter().write(JSON.toJSONString(ResponseResult.failed(ResultEnum.USER_LOGOUT_SUCCESS.getMessage(), ResultEnum.USER_LOGOUT_SUCCESS.getCode()))); } }
- 编写会话信息过期策略类
package com.gremlin.security.strategy; import com.alibaba.fastjson2.JSON; import com.gremlin.common.resp.ResponseResult; import com.gremlin.common.resp.ResultEnum; import org.springframework.security.web.session.SessionInformationExpiredEvent; import org.springframework.security.web.session.SessionInformationExpiredStrategy; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Description: 会话信息过期策略 * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Component public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException { HttpServletResponse httpServletResponse = sessionInformationExpiredEvent.getResponse(); httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8"); httpServletResponse.getWriter().write(JSON.toJSONString( ResponseResult.failed(ResultEnum.USER_NO_ACCESS.getMessage(),ResultEnum.USER_NO_ACCESS.getCode()))); } }
- 编写结果响应json返回类
package com.gremlin.common.resp; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.io.Serializable; /** * Description: 结果响应json返回类 * @author: gremlin * Date: 2022/6/9 15:54 * @version: 1.0.0 */ @Slf4j @Data public class ResponseResult<T> implements Serializable { @ApiModelProperty(value = "返回数据") private T data; @ApiModelProperty(value = "返回信息") private String msg; @ApiModelProperty(value = "结果code") private Integer code; @ApiModelProperty(value = "是否成功") private Boolean isSuccess = true; public ResponseResult() { } /** * 成功 */ public ResponseResult(T data,Integer code, String msg) { this.code = code; this.msg = msg; this.data = data; } /** * 成功 */ public ResponseResult(T data,Integer code, String msg,Boolean isSuccess) { this.code = code; this.msg = msg; this.data = data; this.isSuccess = isSuccess; } /** * 失败 */ public ResponseResult(String msg,Integer code) { this.code = code; this.msg = msg; } /** * 失败 */ public ResponseResult(String msg) { this.msg = msg; } /** * 方便静态调用(成功) */ public static <T> ResponseResult<T> success(T data, Integer code, String msg) { return new ResponseResult<T>(data,code,msg); } /** * 方便静态调用(成功) */ public static <T> ResponseResult<T> success(T data, Integer code, String msg,Boolean isSuccess) { return new ResponseResult<T>(data,code,msg,isSuccess); } /** * 方便静态调用(失败) */ public static <T> ResponseResult<T> failed(String msg,Integer code) { return new ResponseResult<T>(msg,code); } /** * 方便静态调用(失败) */ public static <T> ResponseResult<T> failed(String msg) { return new ResponseResult<T>(msg); } }
- 编写结果code枚举类
package com.gremlin.common.resp; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; /** * Description: 结果code枚举类 * @author: gremlin * Date: 2022/6/9 16:13 * @version: 1.0.0 */ @Getter public enum ResultEnum { /** * 请求成功 */ SUCCESS(200,"请求成功"), /** * 请求失败 */ FAILURE(102,"请求失败"), /** * 用户未登录 */ USER_NEED_AUTHORITIES(201,"用户未登录"), /** * 用户登录成功 */ USER_LOGIN_SUCCESS(203,"login success!"), /** * 用户账号或密码错误 */ USER_LOGIN_FAILED(202,"用户账号或密码错误"), /** * 用户不存在 */ USER_NOT_FIND(206,"该用户不存在或被禁用请联系管理员"), /** * 用户登出成功 */ USER_LOGOUT_SUCCESS(205,"登出成功!"), /** * 用户无权访问 */ USER_NO_ACCESS(301,"用户无权访问"), /** * 您的操作已超时,请重新登录 */ LOGIN_IS_OVERDUE(204,"您的操作已超时,请重新登录"), /** * 手机号码不正确 */ ERROR_PHONE(50001,"手机号码不正确"), /** * 邮箱地址不正确 */ ERROR_EMAIL(50002,"邮箱地址不正确"), /** * 验证码错误 */ ERROR_CODE(50003,"验证码错误"), /** * 邮件发送成功 */ SEND_EMAIL(205,"邮件发送成功!") ; @ApiModelProperty(value = "返回code") private final Integer code; @ApiModelProperty(value = "返回信息") private final String message; ResultEnum(Integer code, String message) { this.code = code; this.message = message; } }
- 编写工具类ip地址获取工具类
package com.gremlin.common.utils; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * Description: ip地址获取工具类 * @author: gremlin * Date: 2022/6/10 15:11 * @version: 1.0.0 */ @Component public class AccessAddressUtils { /** * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址, * 参考文章: http://developer.51cto.com/art/201111/305181.htm * * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢? * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。 * * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, * 192.168.1.100 * * 用户真实IP为: 192.168.1.110 */ public static String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
- 编写工具类日期处理工具类
package com.gremlin.common.utils; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.GregorianCalendar; /** * Description: 日期处理工具类 * @author: gremlin * Date: 2022/6/10 15:17 * @version: 1.0.0 */ public class DateUtil { private final static SimpleDateFormat SDF_TIME = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final SimpleDateFormat SDF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); /** * 获取当前时间的YYYY-MM-DD HH:mm:ss格式 */ public static String getTime() { return SDF_TIME.format(new Date()); } /** * 日期比较,如果s>=e 返回true 否则返回false */ public static boolean compareDate(String s, String e) { if(fomatDate(s)==null||fomatDate(e)==null){ return false; } return s.compareTo(e)>0; } /** * 日期比较,如果s>=e 返回true 否则返回false */ public static boolean compareDate(Date s, Date e) { if(s==null||e==null){ return false; } return s.getTime()-e.getTime()<=5*60*1000; } /** * 格式化日期 */ public static Date fomatDate(String date) { DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); try { return fmt.parse(date); } catch (ParseException e) { e.printStackTrace(); return null; } } /** * 获取当前时间的后i天 */ public static String getAddDay(int i){ String currentTime = DateUtil.getTime(); GregorianCalendar gCal = new GregorianCalendar( Integer.parseInt(currentTime.substring(0, 4)), Integer.parseInt(currentTime.substring(5, 7)) - 1, Integer.parseInt(currentTime.substring(8, 10))); gCal.add(GregorianCalendar.DATE, i); return SDF_DATE_FORMAT.format(gCal.getTime()); } /** * 获取当前时间的后i天 精确到秒 */ public static String getAddDayTime(int i){ Date date = new Date(System.currentTimeMillis()+ (long) i *24*60*60*1000); return SDF_TIME.format(date); } /** * 获取当前时间的+多少秒 精确到秒 */ public static String getAddDaySecond(int i){ Date date = new Date(System.currentTimeMillis()+i* 1000L); return SDF_TIME.format(date); } }
- 编写Jwt生成token工具类
package com.gremlin.common.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.CompressionCodecs; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.swagger.annotations.ApiModelProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.Date; import java.util.Map; /** * Description: Jwt生成token工具类 * @author: gremlin * Date: 2022/6/10 15:17 * @version: 1.0.0 */ @Slf4j public class JwtTokenUtils { @ApiModelProperty(value = "令牌签名密钥") private static final String tokenSignKey = "gremlin"; @ApiModelProperty(value = "token有效时长30分钟") private static final long tokenExpiration = 30*60*1000; /** * 生成token */ public static String generateToken(String subject, Map<String,Object> claims) { return Jwts.builder().setClaims(claims) .setSubject(subject) .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) // 签名算法 .signWith(SignatureAlgorithm.HS512, tokenSignKey) // 压缩格式 .compressWith(CompressionCodecs.GZIP).compact(); } /** * 判断令牌是否过期 */ public static Boolean isTokenExpired(String token) { try { return getTokenBody(token).getExpiration().before(new Date()); } catch (Exception e) { return false; } } /** * 解析token,获得subject中的信息 */ public static String parseToken(String token) { String subject = null; try { subject = getTokenBody(token).getSubject(); } catch (Exception e) { log.info(String.valueOf(e)); } return subject; } /** * 获取token自定义属性 */ public static Map<String,Object> getClaims(String token){ Map<String,Object> claims = null; try { claims = getTokenBody(token); }catch (Exception e) { e.printStackTrace(); } return claims; } public static Date getTokenTime(String token){ return getTokenBody(token).getExpiration(); } /** * 是否已过期 */ public static boolean isExpiration(String expirationTime){ //通过redis中的失效时间进行判断 String currentTime = DateUtil.getTime(); if(DateUtil.compareDate(currentTime,expirationTime)){ //当前时间比过期时间大,失效 return true; }else{ return false; } } private static Claims getTokenBody(String token){ return Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody(); } /** * 根据token字符串获取账号 * @param request * @return */ public static String getUserByJwtToken(HttpServletRequest request) { String jwtToken = request.getHeader("token"); if(!StringUtils.hasText(jwtToken)) { return ""; } Claims tokenBody = getTokenBody(jwtToken); return tokenBody.getSubject(); } }
- 编写redis工具配置类
package com.gremlin.common.utils; import com.gremlin.security.vo.User; import io.swagger.annotations.ApiModelProperty; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Description: redis工具类 * @author: gremlin * Date: 2022/6/10 14:43 * @version: 1.0.0 */ @Component public class RedisUtils { @Autowired private StringRedisTemplate redisTemplate; @ApiModelProperty(value = "reids 中用户信息吗存储2小时 刷新时间") private final int validTime = 2; @ApiModelProperty(value = "过期时间 秒") private final int expirationSeconds = 300; /** * 删除精确key值下所有值 */ public void deleteKey(String key) { redisTemplate.opsForHash().getOperations().delete(key); } /** * 删除key前缀值下所有值 */ public void deleteKeys(String key) { //最后一定要带上 * Set<String> keys = redisTemplate.keys(key + "*"); if (CollectionUtils.isNotEmpty(keys)){ redisTemplate.delete(keys); } } /** * 删出key 这里跟下边deleteKey()最底层实现都是一样的,应该可以通用 */ public void delete(String key){ redisTemplate.opsForValue().getOperations().delete(key); } /** * 字符串获取值 */ public Object get(String key){ return redisTemplate.opsForValue().get(key); } /** * 字符串存入值 默认过期时间为2小时 */ public void set(String key, String value){ redisTemplate.opsForValue().set(key,value, 7200, TimeUnit.SECONDS); } /** * 字符串存入值 */ public void set(String key, String value,Integer expire){ redisTemplate.opsForValue().set(key,value, expire,TimeUnit.SECONDS); } /** * 查询token下的刷新时间 */ public Object getTokenValidTimeByToken(String token) { return redisTemplate.opsForHash().get(token, "tokenValidTime"); } /** * 查询token下的刷新时间 * * @param token 查询的key * @return HV */ public Object getUsernameByToken(String token) { return redisTemplate.opsForHash().get(token, "username"); } /** * 查询token下的刷新时间 * * @param token 查询的key * @return HV */ public Object getIpByToken(String token) { return redisTemplate.opsForHash().get(token, "ip"); } public void setTokenRefresh(String username,String token,String ip){ //刷新时间 Integer expire = validTime*60*60*1000; hset(username, "tokenValidTime",DateUtil.getAddDayTime(validTime),expire); hset(username, "expirationTime",DateUtil.getAddDaySecond(expirationSeconds),expire); hset(username, "username",username,expire); hset(username, "token",token,expire); hset(username, "ip",ip,expire); } /** * 添加单个 */ public void hset(String key,String filed,Object domain,Integer expire){ redisTemplate.opsForHash().put(key, filed, domain); redisTemplate.expire(key, expire,TimeUnit.SECONDS); } /** * 判断key和field下是否有值 */ public Boolean hasKey(String key,String field) { return redisTemplate.opsForHash().hasKey(key,field); } /** * 判断key下是否有值 */ public Boolean hasKey(String key) { return redisTemplate.opsForHash().getOperations().hasKey(key); } /** * 查询key和field所确定的值 */ public Object hasGet(String key,String field) { return redisTemplate.opsForHash().get(key, field); } /** * 查询该key下所有值 */ public Object hasGet(String key) { return redisTemplate.opsForHash().entries(key); } /** * 获取用户登录信息 */ public static User getUser(){ // 获取当前用户 UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); User user = new User(); user.setUsername(userDetails.getUsername()); return user; } }
- 编写token响应类 获取token
package com.gremlin.common.utils; import com.gremlin.security.vo.User; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletResponse; /** * Description: token响应类 获取token * @author: gremlin * Date: 2022/6/10 10:55 * @version: 1.0.0 */ @Service public class TokenResponse { @Autowired private RedisUtils redisUtil; public void getResponse(HttpServletResponse response) { //获取用户登录信息 User user = RedisUtils.getUser(); // 获取Token Object token = redisUtil.get(user.getUsername()+"token"); if (StringUtils.isNotBlank((String)token)) { response.reset(); response.setHeader("token", String.valueOf(token)); } else { response.reset(); response.setHeader("token", String.valueOf(token)); } } }
- mapper类以及数据库查询类很简单就不写了
追加:WebSecurityConfigurerAdapter和authenticationManager()过时配置SecurityConfig
package com.gremlin.config; import com.gremlin.common.utils.RedisUtils; import com.gremlin.log.service.LogService; import com.gremlin.power.mapper.PowerManagerMapper; import com.gremlin.power.service.PowerManagerService; import com.gremlin.security.fifter.JwtAuthenticationTokenFilter; import com.gremlin.security.fifter.TokenLoginFilter; import com.gremlin.security.handler.AjaxAccessDeniedHandler; import com.gremlin.security.handler.AjaxAuthenticationEntryPoint; import com.gremlin.security.handler.AjaxLogoutSuccessHandler; import com.gremlin.security.strategy.CustomizeSessionInformationExpiredStrategy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * Description: * * @author: gremlin * Date: 2022/7/12 17:10 * @version: 1.0.0 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { /** * 注销成功返回的 JSON 格式数据给前端 */ @Autowired private AjaxLogoutSuccessHandler logoutSuccessHandler; /** * 无权访问 JSON 格式的数据 */ @Autowired private AjaxAccessDeniedHandler ajaxAccessDeniedHandler; @Autowired private AjaxAuthenticationEntryPoint authenticationEntryPoint; @Autowired private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy; @Autowired private JwtAuthenticationTokenFilter tokenAuthenticationFilter; @Autowired private RedisUtils redisUtils; @Autowired private LogService logService; @Autowired private PowerManagerMapper powerManagerMapper; @Autowired private PowerManagerService powerManagerService; /** * 注入AuthenticationConfiguration */ @Autowired private AuthenticationConfiguration auth; /** * 编写AuthenticationManager的bean */ @Bean public AuthenticationManager authenticationManager() throws Exception { return auth.getAuthenticationManager(); } /** * 替换旧版本中的configure(HttpSecurity http)方法 */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(); http.httpBasic() .authenticationEntryPoint(authenticationEntryPoint) .and() .authorizeRequests() //自定义放行接口 .antMatchers( "/swagger**/**", "/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/v3/**" ).permitAll() .anyRequest() .authenticated() .and().logout().logoutUrl("/logout") //登出处理 .logoutSuccessHandler(logoutSuccessHandler) //添加关于自定义的认证过滤器和自定义的授权过滤器 .and() .logout().permitAll()//注销行为任意访问 //会话管理 .and().sessionManagement() //同一账号同时登录最大用户数 .maximumSessions(1) //会话信息过期策略会话信息过期策略(账号被挤下线) .expiredSessionStrategy(sessionInformationExpiredStrategy); //自定义权限拒绝处理类 // 无权访问 JSON 格式的数据 http.exceptionHandling().accessDeniedHandler(ajaxAccessDeniedHandler); // 登录验证 http.addFilter(new TokenLoginFilter(authenticationManager(),redisUtils,logService,powerManagerMapper,powerManagerService)).httpBasic(); // JWT Filter http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring().antMatchers("/css/**","/js/**","/img/**","/uploads/**","**/favicon.ico"); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
转:https://blog.csdn.net/rq12345688/article/details/125479657
https://docs.spring.io/spring-security/reference/servlet/configuration/java.html