基于Spring Security去实现sso单点登陆和鉴权
因为看了spring security在自己无聊的时候书写的demo发现有缺陷后来去进行修改完整了。对鉴权需要看的源码还有登陆的实现返回的加密信息进行鉴定等等做了修改
1.首先我们需要去配置到我们spring security的一个配置类去实现我们的WebSecurityConfigurerAdapter接口重写他的配置的信息
/** * @Author: E-mail: * @Date:2022/10/20 17:13 * @Company: * @Version:V1.0 */ @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new MyPasswordEncoder()); } @Autowired private CaptchaFilter captchaFilter; /** * 安全配置 */ @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class); // 跨域共享 http.cors() .and() // 跨域伪造请求限制无效 .csrf().disable() .authorizeRequests() //放行验证码和用户注册的请求 .antMatchers("/login/captcha","/login/usersign").permitAll() .anyRequest() .authenticated() .and() .logout() .logoutRequestMatcher(new OrRequestMatcher( //指定注销登录的接口和方式 new AntPathRequestMatcher("/login/logout","GET") )) .invalidateHttpSession(true)//是否让当前的session失效 .clearAuthentication(true)//清除认证标记 .logoutSuccessHandler(new MyLogoutSuccessHandler()) .and() // 添加JWT登录拦截器 .addFilter(new JWTAuthenticationFilter(authenticationManager())) // 添加JWT鉴权拦截器 .addFilter(new JWTAuthorizationFilter(authenticationManager())) .sessionManagement() // 设置Session的创建策略为:Spring Security永不创建HttpSession 不使用HttpSession来获取SecurityContext .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 异常处理 .exceptionHandling() .accessDeniedHandler(new MyAccessDeniedHandler()) // 匿名用户访问无权限资源时的异常 .authenticationEntryPoint(new JWTAuthenticationEntryPoint()); } /** * 跨域配置 * @return 基于URL的跨域配置信息 */ @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 注册跨域配置 source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
当校验成功会去调用到我们配置了UserDetailsService的实现类去进行鉴权和判断那么这个时候我们只需要去实现到我们的UserDetailsService这个时候当登陆以后就会调用到你的实现类
@Service class UserDetailsServiceImpl implements UserDetailsService { @Autowired(required = false) private TUserMapper tUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (username == null || "".equals(username)) { throw new RuntimeException("用户不能为空"); } TUser tUser = tUserMapper.loginByUsername(username); if (tUser == null){ throw new RuntimeException("用户不存在"); } List<TRole> tRoles = tUser.gettRoles(); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); tRoles.forEach(param -> { authorities.add(new SimpleGrantedAuthority("ROLE_"+param.getRoleName())); }); return new org.springframework.security.core.userdetails.User(tUser.getUsername(),tUser.getPassword(),authorities); } }
那么当如果我们登陆成功和注销成功以后呢?应该怎么处理按照sso单点登陆的话我们应该返回一个凭证给到调用方这个时候我们应该怎么去返回我们就需要去实现到他的实现类
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Autowired private RedisStringCache redisStringCache; @Resource private TUserMapper tUserMapper; private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; if (tUserMapper == null){ this.tUserMapper = (TUserMapper) SpringUtils.getBean(TUserMapper.class); } if (redisStringCache == null){ this.redisStringCache = (RedisStringCache) SpringUtils.getBean(RedisStringCache.class); } } /** * 验证操作 接收并解析用户凭证 */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 从输入流中获取到登录的信息 // 创建一个token并调用authenticationManager.authenticate() 让Spring security进行验证 return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getParameter("username"),request.getParameter("password"))); } /** * 验证【成功】后调用的方法 * 若验证成功 生成token并返回 */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException { User user= (User) authResult.getPrincipal(); // 从User中获取权限信息 Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); // 创建Token String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString()); //添加登陆数据进入redis中 redisStringCache.saveCache(JwtTokenUtil.TOKEN_PREFIX + token,user.getUsername(), CacheType.LOGIN); // 设置编码 防止乱码问题 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); // 在请求头里返回创建成功的token // 设置请求头为带有"Bearer "前缀的token字符串 //response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token); // 处理编码方式 防止中文乱码 response.setContentType("text/json;charset=utf-8"); // 将反馈塞到HttpServletResponse中返回给前台 response.setCharacterEncoding("utf-8"); response.setContentType("text/javascript;charset=utf-8"); TUserExample tUserExample = new TUserExample(); tUserExample.createCriteria().andUsernameEqualTo(user.getUsername()); List<TUser> tUsers = tUserMapper.selectByExample(tUserExample); response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseMeaage.sucess(JwtTokenUtil.TOKEN_PREFIX + token,0,tUsers.get(0).getLoginName()))); } /** * 验证【失败】调用的方法 */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { String returnData=""; // 账号过期 if (failed instanceof AccountExpiredException) { returnData="账号过期"; } // 密码错误 else if (failed instanceof BadCredentialsException) { returnData="密码错误"; } // 密码过期 else if (failed instanceof CredentialsExpiredException) { returnData="密码过期"; } // 账号不可用 else if (failed instanceof DisabledException) { returnData="账号不可用"; } //账号锁定 else if (failed instanceof LockedException) { returnData="账号锁定"; } // 用户不存在 else if (failed instanceof InternalAuthenticationServiceException) { returnData="用户不存在"; } // 其他错误 else{ returnData="未知异常"; } // 处理编码方式 防止中文乱码 response.setContentType("text/json;charset=utf-8"); // 将反馈塞到HttpServletResponse中返回给前台 response.getWriter().write(JSON.toJSONString(returnData)); } }
当没有访问权限没有应该
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("text/javascript;charset=utf-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseMeaage.error("您未登录,没有访问权限"))); } }
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); PrintWriter out= response.getWriter(); out.write(new ObjectMapper().writeValueAsString(ResponseMeaage.error("权限不足"))); //将对象转json输出 out.flush(); out.close(); } }
public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String authorization = request.getHeader("Authorization"); response.setCharacterEncoding("utf-8"); response.setContentType("text/javascript;charset=utf-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseMeaage.sucess(null,0,"注销成功"))); } }
登陆成功鉴权
public class JWTAuthorizationFilter extends BasicAuthenticationFilter { @Autowired private RedisStringCache redisStringCache; public JWTAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } /** * 在过滤之前和之后执行的事件 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER); // 若请求头中没有Authorization信息 或是Authorization不以Bearer开头 则直接放行 if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) { chain.doFilter(request, response); return; } SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader)); super.doFilterInternal(request, response, chain); } /** * 从token中获取用户信息并新建一个token * * @param tokenHeader 字符串形式的Token请求头 这边可以增加对应的一个校验哦 * @return 带用户名和密码以及权限的Authentication */ private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) { // 去掉前缀 获取Token字符串 String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, ""); // 从Token中解密获取用户名 String username = JwtTokenUtil.getUsername(token); // 从Token中解密获取用户角色 String role = JwtTokenUtil.getUserRole(token); // 将[ROLE_XXX,ROLE_YYY]格式的角色字符串转换为数组 String[] roles = StringUtils.strip(role, "[]").split(", "); Collection<SimpleGrantedAuthority> authorities=new ArrayList<>(); for (String s:roles) { authorities.add(new SimpleGrantedAuthority(s)); } if (username != null) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities); return usernamePasswordAuthenticationToken; } return null; } /** * 【失败】调用的方法 */ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { String returnData=""; // 账号过期 if (failed instanceof AccountExpiredException) { returnData="账号过期"; } // 密码错误 else if (failed instanceof BadCredentialsException) { returnData="密码错误"; } // 密码过期 else if (failed instanceof CredentialsExpiredException) { returnData="密码过期"; } // 账号不可用 else if (failed instanceof DisabledException) { returnData="账号不可用"; } //账号锁定 else if (failed instanceof LockedException) { returnData="账号锁定"; } // 用户不存在 else if (failed instanceof InternalAuthenticationServiceException) { returnData="用户不存在"; } // 其他错误 else{ returnData="未知异常"; } // 处理编码方式 防止中文乱码 response.setContentType("text/json;charset=utf-8"); // 将反馈塞到HttpServletResponse中返回给前台 response.getWriter().write(JSON.toJSONString(returnData)); } }
当我们需要去添加验证码和校验验证码
** * @Author: E-mail: * @Date:2022/10/25 8:59 * @Company: * @Version:V1.0 */ @RestController @RequestMapping("/login") @Log4j2 public class LoginController { @Autowired @Qualifier("loginService") private LoginService loginService; /** * @Company 登陆验证码进行放行 */ @RequestMapping("/captcha") public ResponseVo capecha(HttpServletRequest request) throws IOException { //1.生成验证码 120长,高40,4个字符,藻条+线条 Captcha captcha = new Captcha(120, 40, 4, 10); //2.把验证码放入缓存ID 验证码数值 //生成随机的uuid确保不会重复 String uuid = String.valueOf(GudyUuid.getInstance().getUUID()); HttpSession session = request.getSession(); //用于操作redis后期添加redis配置可以添加 RedisStringCache.saveCache(uuid,captcha.getCode(), CacheType.CAPTCHA); log.info("生成验证码成功标识为:{}登陆验证码{}",uuid,captcha.getCode()); //3.base64编码图片返回前台 CaptchaRes captchaRes = new CaptchaRes(); captchaRes.setId(uuid); captchaRes.setImageBase64(captcha.getBasee64ByteStr()); return ResponseMeaage.sucess(captchaRes,0,"获取验证码成功"); } @GetMapping("/logout") public void logout(HttpServletRequest request){ String token = request.getHeader("Authorization"); //删除redis } //用户注册 @PostMapping("/usersign") public ResponseVo userSign(TUser tUser){ return loginService.saveUser(tUser); } }
public class GudyUuid { private static GudyUuid ourInstance = new GudyUuid(); public static GudyUuid getInstance() { return ourInstance; } private GudyUuid() { } public void init(long centerId, long workerId) { idWorker = new SnowflakeIdWorker(workerId, centerId); } private SnowflakeIdWorker idWorker; public long getUUID() { return idWorker.nextId(); } }
public class SnowflakeIdWorker { // ==============================Fields=========================================== /** 开始时间截 (2015-01-01) */ private final long twepoch = 1420041600000L; /** 机器id所占的位数 */ private final long workerIdBits = 5L; /** 数据标识id所占的位数 */ private final long datacenterIdBits = 5L; /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** 支持的最大数据标识id,结果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** 序列在id中占的位数 */ private final long sequenceBits = 12L; /** 机器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** 数据标识id向左移17位(12+5) */ private final long datacenterIdShift = sequenceBits + workerIdBits; /** 时间截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** 工作机器ID(0~31) */ private long workerId; /** 数据中心ID(0~31) */ private long datacenterId; /** 毫秒内序列(0~4095) */ private long sequence = 0L; /** 上次生成ID的时间截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 构造函数 * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } // ==============================Methods========================================== /** * 获得下一个ID (该方法是线程安全的) * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 if (timestamp < lastTimestamp) { throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒内序列溢出 if (sequence == 0) { //阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } //时间戳改变,毫秒内序列重置 else { sequence = 0L; } //上次生成ID的时间截 lastTimestamp = timestamp; //移位并通过或运算拼到一起组成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一个毫秒,直到获得新的时间戳 * @param lastTimestamp 上次生成ID的时间截 * @return 当前时间戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒为单位的当前时间 * @return 当前时间(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } //==============================Test============================================= /** 测试 */ public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } }
@Component @Log4j2 public class CaptchaFilter extends GenericFilterBean { @Value("${iscaptcha}") private boolean iscaptcha; @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if(StringUtils.equals("/login",request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(),"post") && iscaptcha){ //验证谜底与用户输入是否匹配 ResponseVo validate = validate(request); if (validate.getSuccess() != 200){ response.setCharacterEncoding("utf-8"); response.setContentType("text/javascript;charset=utf-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseMeaage.error( validate.getMsg()))); return; } } chain.doFilter(request, response); } //校验规则 private ResponseVo validate(HttpServletRequest request) throws ServletRequestBindingException { //验证码缓存到redis中而不是session,占时放在session中做模拟测试 HttpSession session = request.getSession(); String code = request.getParameter("code"); String captchaId = request.getParameter("captchaId"); if(StringUtils.isEmpty(code)){ return ResponseMeaage.error("验证码不能为空"); } // 获取缓存 String token = RedisStringCache.queryCache(captchaId, CacheType.CAPTCHA); if(Objects.isNull(token)) { return ResponseMeaage.error("验证码不存在"); } // 请求验证码校验 if(!StringUtils.equalsIgnoreCase(token, code)) { return ResponseMeaage.error("验证码不匹配"); } //失效验证码 RedisStringCache.removeCache(captchaId, CacheType.CAPTCHA); return ResponseMeaage.sucess(null,0,"校验成功"); } }
@RestController public class TestController { @PreAuthorize("hasRole('USER')") @GetMapping("/data") public String data() { return "This is data."; } @PreAuthorize("hasRole('ADMIN')") @PostMapping("/sd") public String ee(){ return "#24234"; } @PostMapping("/aa") public String aa(){ return "12345"; } }
浙公网安备 33010602011771号