SpringSecurity前后端分离下的方案

四、SpringSecurity前后端分离下的方案

1、基本原理:

  1. 登录过程是SpringSecurity原理,然后验证成功后利用Jwt生产用户Token,用Key为Token,Value为用户信息存入Redis中完成首次登录。
  2. 之后的请求中,过滤器去判断请求中是否携带了Token,如果有就直接放行继续接下来的操作,否则无权访问需要登录。
  3. 思路流程图:
    • 在这里插入图片描述

2、详细代码流程

  1. 编写核心配置了

    • @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
      
          @Autowired
          private AuthenticationSuccessHandler authenticationSuccessHandler;
          @Autowired
          private AuthenticationFailureHandler authenticationFailureHandler;
          @Autowired
          private LogoutSuccessHandler logoutSuccessHandler;
          @Autowired
          private AuthenticationEntryPoint authenticationEntryPoint;
          @Autowired
          private UserDetailsService userDetailsService;
          @Autowired
          private TokenFilter tokenFilter;
      
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              //关闭CSRF防护
              http.csrf().disable();
      
              //基于Token,不需要Session
              http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
      
              //对于路径请求
              http.authorizeRequests()
                      .antMatchers("/user/all","/user/login").permitAll()//这些路径放行
                      .anyRequest()//其他任何请求
                      .authenticated();//都需要验证
      
              http.formLogin()
                  	//登录的路径请求,usernameParameter设置了表单中username的参数名,passwordParameter设置了表单中password的参数名
                      .loginProcessingUrl("/user/login").usernameParameter("username").passwordParameter("password")
                  	//成功会进入这个处理器
                      .successHandler(authenticationSuccessHandler)
                  	//失败会进入这个处理器
                      .failureHandler(authenticationFailureHandler)
                  	//没有权限会进入这个处理器
                      .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
              		
              //退出的路径请求,logoutSuccessHandler表示退出成功后进入这个处理器
              http.logout().logoutUrl("user/logout").logoutSuccessHandler(logoutSuccessHandler);
      
              //表示在UsernamePasswordAuthenticationFilter这个过滤器执行前,先执行tokenFilter过滤器
              http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
          }
      
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
                  @Override
                  public String encode(CharSequence charSequence) {
                      return MD5.encrypt(charSequence.toString());
                  }
      
                  @Override
                  public boolean matches(CharSequence charSequence, String s) {
                      String encode = MD5.encrypt(charSequence.toString());
                      return s.equals(encode);
                  }
              });
          }
      }
      
  2. 编写TokenFilter过滤器

    • 主要功能:验证前去过滤请求中是否包含了token,如果包含了就从Redis中获取用户信息,否则继续验证

    • @Component
      @Slf4j
      @Order(value = Integer.MAX_VALUE - 2)
      public class TokenFilter extends OncePerRequestFilter {
      
          public static final String TOKEN_KEY = "Authorization";
      
          @Autowired
          private TokenService tokenService;
      
          @Autowired
          private UserDetailsService userDetailsService;
      
          private static final Long MINUTES_10 = 10 * 60 * 1000L;
      
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
              //从请求中获取token
              String token = getToken(request);
      
              //如果token不为空
              if(!StringUtils.isEmpty(token)){
                  //就从redis中获取用户信息
                  LoginUser loginUser = tokenService.getLoginUser(token);
                  if(loginUser != null ){
                      //如果不为空,就检查缓存中的时间是否小于10分钟,如果小于就更新缓存
                      loginUser =checkLoginTime(loginUser);
                      //把用户对象封装成UsernamePasswordAuthenticationToken对象
                      UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
                      //把UsernamePasswordAuthenticationToken对象放入Security上下文中
                      SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                  }
              }
      
              filterChain.doFilter(request,response);
          }
      
          public static String getToken(HttpServletRequest request) {
              String token = request.getParameter(TOKEN_KEY);
              if (StringUtils.isEmpty(token)) {
                  token = request.getHeader(TOKEN_KEY);
              }
              if (StringUtils.isEmpty(token)) {
                  Cookie[] cookies = request.getCookies();
                  if (cookies != null) {
                      for (Cookie cookie : cookies) {
                          if (TOKEN_KEY.equals(cookie.getName())) {
                              token = cookie.getValue();
                              break;
                          }
                      }
                  }
              }
              return token;
          }
      
          /**
           * 校验时间<br>
           * 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
           *
           * @param loginUser
           * @return
           */
          public LoginUser checkLoginTime(LoginUser loginUser) {
              long expireTime = loginUser.getExpireTime();
              long currentTime = System.currentTimeMillis();
              if (expireTime - currentTime <= MINUTES_10) {
                  String token = loginUser.getToken();
      
                  loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
                  loginUser.setToken(token);
                  tokenService.refresh(loginUser);
              }
              return loginUser;
          }
      }
      
  3. 编写Handler配置类

    • @Configuration
      public class SecurityHandlerConfig {
      
          @Autowired
          private TokenService tokenService;
      
      
          
      	//登录成功后的处理器
          @Bean
          public AuthenticationSuccessHandler loginSuccsessHandler(){
              return (request, response, authentication) -> {
                  //从SpringSecurity上下文中获取已经通过认证的用户对象
                  LoginUser loginUser = (LoginUser) authentication.getPrincipal();
      			
                  //登录成功的相应逻辑操作
                  loginSuccessReturn(request,response,loginUser);
              };
          }
          
          public void loginSuccessReturn(HttpServletRequest request, HttpServletResponse response, LoginUser loginUser) {
              //响应容器
              Map map = new HashMap();
      		
              //根据用户生产一个Token,并存入redis
              Token token = tokenService.saveToken(loginUser);
              
              //放入加密token
              map.put("id", loginUser.getId());
              map.put("token", token.getToken());
      
      		
              Cookie cookie = new Cookie("token", map.get("token").toString());
              cookie.setPath("/");
              response.addCookie(cookie);
              //封装返回
              responseJson(response, HttpStatus.OK.value(), map);
          }
      
      	//登录失败的处理器
          @Bean
          public AuthenticationFailureHandler loginFailureHandler(){
              return (request, response, exception) -> {
                  String msg;
                  if(exception instanceof BadCredentialsException){
                      msg = "密码错误!";
                  } else {
                      msg = exception.getMessage();
                  }
                  Map<String, Object> data = new HashMap<>();
                  data.put("loginType", 5);
                  data.put("loginMsg", msg);
                  responseJson(response, HttpStatus.OK.value(), data);
              };
          }
      
          //无权限处理器
          @Bean
          public AuthenticationEntryPoint authenticationEntryPoint(){
              return (request, response, exception) -> {
                  String url = request.getRequestURI();
                  if(url.endsWith(".html")) {
                      response.sendRedirect("/");
                  } else {
                      responseJson(response, HttpStatus.UNAUTHORIZED.value(), "请先登录");
      
                  }
              };
          }
      
          //登出处理器
          @Bean
          public LogoutSuccessHandler logoutSuccessHandler(){
              return (request, response, authentication) -> {
      
                  String token = TokenFilter.getToken(request);
                  tokenService.deleteToken(token);
      
                  responseJson(response, HttpStatus.OK.value(), "退出成功");
              };
          }
      
          //封装返回
          public void responseJson(HttpServletResponse response, int status, Object data) {
              try {
                  response.setHeader("Access-Control-Allow-Origin", "*");
                  response.setHeader("Access-Control-Allow-Methods", "*");
                  response.setContentType("application/json;charset=UTF-8");
                  response.setStatus(status);
      
                  response.getWriter().write(JSONObject.toJSONString(data));
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      
  4. 编写UserDetailsService接口实现类

    • @Service
      public class MyUserDetailsService implements UserDetailsService {
      
          @Autowired
          private UserMapper userMapper;
      
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              User user = null;
              if (StringUtils.isEmpty(username) || (user = getByUserName(username)) == null) {
                  // 返回用户名不存在
                  // 抛出异常后 框架会去调用 loginFailureHandler()
                  throw new RuntimeException("用户名不存在");
              }
      		
              LoginUser loginUser = new LoginUser();
              if(user != null ){
                  BeanUtils.copyProperties(user,loginUser);
                  //设置用户权限,可自行修改从数据库获取
                  if(loginUser.getUsername().equals("admin")){
                      List<String> authorities = new ArrayList<String>();
                      authorities.add("ROLE_admin");
                      loginUser.setPermissionValueList(authorities);
                  } else {
                      List<String> authorities = new ArrayList<String>();
                      authorities.add("ROLE_admin1");
                      loginUser.setPermissionValueList(authorities);
                  }
              }
              return loginUser;
          }
      
          private User getByUserName(String username) {
              QueryWrapper<User> query = new QueryWrapper<>();
              query.lambda().eq(User::getUsername,username);
              User user = userMapper.selectOne(query);
              return user;
          }
      
  5. 编写UserDetails接口实体类

    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class LoginUser extends User implements UserDetails {
      
          private String token;
      
          private Long loginTime;
      
          private Long expireTime;
      	
          private List<String> permissionValueList;
      
          private List<GrantedAuthority> authorities;
      
      
          //获得用户权限
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              authorities = new ArrayList<>();
              for (String permissionValue : permissionValueList) {
                  if(StringUtils.isEmpty(permissionValue))continue;
                  SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                  authorities.add(authority);
              }
              return authorities;
      //        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;
          }
      }
      
    • //User实体类对应数据库表的实体类
      @Data
      public class User {
          private Integer id;
          private String username;
          private String password;
      
      }
      
  6. 提供个人写的TokenService供大家参考,也可自行编写(主要逻辑是生产Token,存入Redis)

    • @Service
      @Slf4j
      public class TokenService {
      
          /**
           * token过期秒数
           */
          @Value("${token.expire.seconds}")
          private Integer expireSeconds;
      
          /**
           * 私钥
           */
          @Value("${token.jwtSecret}")
          private String jwtSecret;
          
          @Autowired
          private JedisClient jedisClient;
      
          private static Key KEY = null;
      
          private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY";
      
          /**
           * 保存用户信息至缓存,key为uuid,返回生成token
           * @param loginUser
           * @return
           */
          public Token saveToken(LoginUser loginUser) {
              loginUser.setToken(UUID.randomUUID().toString());
              loginUser.setLoginTime(System.currentTimeMillis());
              loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000);
      
              jedisClient.setnx(loginUser.getToken(), JSONObject.toJSONString(loginUser), Long.valueOf(expireSeconds * 1000));
      
              String jwtToken = createJWTToken(loginUser);
      
              return new Token(jwtToken, loginUser.getLoginTime());
          }
      
          /**
           * 生成jwt
           *
           * @param loginUser
           * @return
           */
          private String createJWTToken(LoginUser loginUser) {
              Map<String, Object> claims = new HashMap<>();
              // 放入一个随机字符串,通过该串可找到登陆用户
              claims.put(LOGIN_USER_KEY, loginUser.getToken());
      
              String jwtToken = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance())
                      .compact();
      
              return jwtToken;
          }
      
          /**
           * 刷新缓存
           * @param loginUser
           */
          public void refresh(LoginUser loginUser) {
              loginUser.setLoginTime(System.currentTimeMillis());
              loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000);
      
              jedisClient.setnx(loginUser.getToken(), JSONObject.toJSONString(loginUser), Long.valueOf(expireSeconds * 1000));
          }
      
          /**
           * 根据jwt获取登录用户信息
           * @param jwtToken
           * @return
           */
          public LoginUser getLoginUser(String jwtToken) {
              String uuid = getUUIDFromJWT(jwtToken);
              if (uuid != null) {
                  return toLoginUser(uuid);
              }
              return null;
          }
      
          /**
           * 删除缓存中的用户信息
           * @param jwtToken
           * @return
           */
          public boolean deleteToken(String jwtToken) {
              String uuid = getUUIDFromJWT(jwtToken);
              if (uuid != null) {
                  LoginUser loginUser = toLoginUser(uuid);
                  if (loginUser != null) {
                      jedisClient.del(uuid);
                      return true;
                  }
              }
              return false;
          }
      
          /**
           * 加锁获取加密key
           * @return
           */
          private Key getKeyInstance() {
              if (KEY == null) {
                  synchronized (TokenService.class) {
                      if (KEY == null) {
                          // 双重锁
                          byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtSecret);
                          KEY = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
                      }
                  }
              }
      
              return KEY;
          }
      
          /**
           * 解析jwt获取uuid
           * @param jwt
           * @return
           */
          private String getUUIDFromJWT(String jwt) {
              if ("null".equals(jwt) || StringUtils.isEmpty(jwt)) {
                  return null;
              }
              Map<String, Object> jwtClaims = null;
              try {
                  jwtClaims = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwt).getBody();
                  if (jwtClaims.containsKey(LOGIN_USER_KEY)) {
                      return (String) jwtClaims.get(LOGIN_USER_KEY);
                  }
                  return null;
              } catch (ExpiredJwtException e) {
                  log.error("token:{}已过期", jwt);
              } catch (Exception e) {
                  log.error("解析token异常,token:{}", e);
              }
              return null;
          }
      
          /**
           * 根据key获取缓存中的用户信息
           * @param key  缓存key
           * @return
           */
          private LoginUser toLoginUser(String key) {
              if (key == null) {
                  return null;
              }
      
              String value = jedisClient.get(key);
              
              // 校验是否已过期,已过期value为null
              if (StringUtils.isNotEmpty(value)) {
                  LoginUser loginUser = JSONObject.parseObject(value, LoginUser.class);
                  return loginUser;
              }
              return null;
          }
      }
      
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值