天天看點

SpringSecurity前後端分離下的方案

四、SpringSecurity前後端分離下的方案

1、基本原理:

  1. 登入過程是SpringSecurity原理,然後驗證成功後利用Jwt生産使用者Token,用Key為Token,Value為使用者資訊存入Redis中完成首次登入。
  2. 之後的請求中,過濾器去判斷請求中是否攜帶了Token,如果有就直接放行繼續接下來的操作,否則無權通路需要登入。
  3. 思路流程圖:
    • SpringSecurity前後端分離下的方案

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;
          }
      }
                 

繼續閱讀