04 Springsecurity+jwt+redis 登陸方案的選擇——從0到1開始登錄

前言

做者從hallo world開始到目前的菜鳥水平,幾乎接觸過的每一個項目都避不開登陸這個功能。從最開始的不瞭解會話機制,密碼帳戶校驗成功只管往session裏存我的信息,到後來的瞭解無狀態、有狀態的登錄的各個方案,不得不說學習得是一個積累得過程,想要完整的瞭解一門技術,一個技術棧,得系統的去學習。想起了以前面試過一個兩年技術的小哥,不知道出於什麼想法,他在簡歷的末尾來了4個餅圖,以百分比的形式去展現掌握程度,當時沒忍住笑了出來(固然不是面試的時候)——java 75%,mysql80%,orcle75%,linux80%,面試一問你看過hashmap的源碼,瞭解底層原理嗎?額~...沒有,不知道。那你用過觸發器,知道什麼是存儲過程嗎?額~...沒有用過...html

其實這種狀況不少見,一些年輕的求職者找工做時用的簡歷都很粗糙,包括做者本身的。可是必定要注意對於本身會的技術棧,你掌握的程度必定要用對關鍵詞。好比一個一年技術經驗的java開發,簡歷上寫着各類掌握、熟練掌握springboot,稍微好一點的公司應該會直接把你刷掉,缺人的公司可能會約面試,可是面試官通常都會很反感。固然若是天賦異稟,真的能在這麼短的時間內達到掌握的程度,那我也無話可說,建議直接投aliP8。java

一、方案的選擇

登陸身份存儲的方案其實不少,但出於不少的項目都須要去考慮項目的安全和權限體系,如今流行的權限框架有shiro,和springsecurity,前者相對用戶羣體會大一些。而登陸總體分爲兩大類:無狀態和有狀態。mysql

  • 有狀態:

    有狀態是指用戶與服務器進行會話,使用session等方式保持,服務端會存留用戶的會話信息。linux

  • 無狀態:

    而無狀態是如今比較流行的方式,通俗的說,服務器不去保留你用戶會話的信息,用戶來訪經過認證受權以後給你的token,存於用戶端,服務端會從token中解析其中的信息,而後根據這些信息去作業務操做。在解析token以前,並不能知道你是誰,服務端也不保留token,不使用session,在訪問量比較大的狀況下會減少服務器壓力。web

所以登陸方案的選擇主要仍是看具體的項目規模,業務的需求。面試

本文(本項目)主要介紹的是Spring security結合jwt、redis去實現登陸。redis

由於業務需求,做者在無狀態的基礎上,將token進行了惟一處理,意味着單點登陸,單token有效處理,採用redis去實現。所以,打破了無狀態。主要是由於單一的jwt的認證方式有一些缺點:spring

  1. token生成以後沒法強迫失效,只能自動過時;
  2. token沒法延時;
  3. 修改用戶信息後沒法同步;
  4. token不能存儲太多信息(做者曾將一整個對象實體,包括全部對象權限對象解析成token,結果request報token過長不讓用);
  5. 基於第一點有個狀況是用戶屢次登陸可以登陸成功,意味着服務器會發放屢次token,且這些token都是有效的;

二、Spring security簡介

spring security 的核心功能主要包括:sql

  • 認證 (你是誰)
  • 受權 (你能幹什麼)
  • 攻擊防禦 (防止僞造身份)

spring security 能夠和shiro同樣實現權限的控制,且相比shiro功能會更強大。Spring Security能夠爲 Spring 應用提供聲明式的安全訪問控制,經過一系列在spring應用上下文中可配置的bean,利用spring ioc 和aop 等功能特性來提供聲明式的安全訪問控制功能(經過註解來控制接口和方法的訪問權限),減小重複的工做。數據庫

三、jwt

JSON Web Token (JWT),是在網絡應用間傳遞信息的一種基於 JSON的開放標準,用戶JSON對象在不一樣系統中進行信息的安全傳輸,主要使用場景用於用戶身份提供者和服務提供者間傳遞被認證的用戶身份信息。

對於Spring security和jwt本文就不作過多的介紹了,着重於實現。

四、開始實現

1) pom導入:

<!-- 此處僅展現security與jwt -->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt</artifactId>
 <version>0.9.1</version>
</dependency>

2) 開始配置文件:

SecurityConfig:

package com.ssk.shop.config.security;
​
import com.ssk.shop.config.security.token.JwtTokenUtil;
import com.ssk.shop.config.security.token.RestAuthenticationEntryPoint;
import com.ssk.shop.config.security.token.RestfulAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
​
import javax.sql.DataSource;
​
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
​
 @Autowired
 private BCryptPasswordEncoder passwordEncoder;
​
 /**
 * 註冊沒有權限的處理器
 */
 @Bean
 public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
 return new RestfulAccessDeniedHandler();
 }
​
 @Bean
 public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
 return new RestAuthenticationEntryPoint();
 }
​
 @Bean
 public BCryptPasswordEncoder passwordEncoder() {
 return new BCryptPasswordEncoder();
 }
​
 // 認證用戶的來源(內存或者數據庫)
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder);
 }
​
 //配置springSecurity相關信息artisanType/get_all
 protected void configure(HttpSecurity http) throws Exception {
 ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
 //不須要保護的資源路徑容許訪問
 for (String url : ignoreUrlsConfig().getUrls()) {
 registry.antMatchers(url).permitAll();
 }
 //容許跨域請求的OPTIONS請求
 registry.antMatchers(HttpMethod.OPTIONS)
 .permitAll();
 // 任何請求須要身份認證
 // 釋放靜態資源,指定資源攔截規則,指定自定義認證頁面,指定退出認證配置,csrf配置
 registry.and()
 .authorizeRequests()
 .anyRequest()
 .authenticated()
 // 關閉跨站請求防禦及不使用session
 .and()
 .csrf()
 .disable()
 .sessionManagement()
 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
 // 自定義權限拒絕處理類
 .and()
 .exceptionHandling()
 .accessDeniedHandler(restfulAccessDeniedHandler())
 .authenticationEntryPoint(restAuthenticationEntryPoint())
 // 自定義權限攔截器JWT過濾器
 .and()
 .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
 }
​
 @Autowired
 DataSource dataSource;
​
 @Bean
 public PersistentTokenRepository getPersistentTokenRepository() {
 JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
 jdbcTokenRepository.setDataSource(dataSource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);
 return jdbcTokenRepository;
 }
​
 @Bean
 public IgnoreUrlsConfig ignoreUrlsConfig() {
 return new IgnoreUrlsConfig();
 }
​
 @Bean
 public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
 return new JwtAuthenticationTokenFilter();
 }
​
 @Bean
 public JwtTokenUtil jwtTokenUtil() {
 return new JwtTokenUtil();
 }
​
 @Bean
 public HttpFirewall httpFirewall() {
 return new DefaultHttpFirewall();
 }
​
}

JwtTokenUtil:

package com.ssk.shop.config.security.token;
​
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
​
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
​
/**
 * jwt token生成工具
 *
 * @author ssk
 * @since 2020-11-03 08:09:00
 */
public class JwtTokenUtil {
 private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
 private static final String CLAIM_KEY_USERNAME = "sub";
 private static final String CLAIM_KEY_CREATED = "created";
 @Value("${jwt.secret}")
 private String secret;
 @Value("${jwt.expiration}")
 private Long expiration;
 @Value("${jwt.tokenHead}")
 private String tokenHead;
​
 /**
 * 根據負責生成JWT的token
 */
 private String generateToken(Map<String, Object> claims) {
 return Jwts.builder()
 .setClaims(claims)
 .setExpiration(generateExpirationDate())
 .signWith(SignatureAlgorithm.HS512, secret)
 .compact();
 }
​
 /**
 * 從token中獲取JWT中的負載
 */
 private Claims getClaimsFromToken(String token) {
 Claims claims = null;
 try {
 claims = Jwts.parser()
 .setSigningKey(secret)
 .parseClaimsJws(token)
 .getBody();
 } catch (Exception e) {
 LOGGER.info("JWT格式驗證失敗:{}", token);
 }
 return claims;
 }
​
 /**
 * 生成token的過時時間
 */
 private Date generateExpirationDate() {
 return new Date(System.currentTimeMillis() + expiration * 1000);
 }
​
 /**
 * 從token中獲取登陸用戶名
 */
 public String getUserNameFromToken(String token) {
 String username;
 try {
 Claims claims = getClaimsFromToken(token);
 username = claims.getSubject();
 } catch (Exception e) {
 username = null;
 }
 return username;
 }
​
 /**
 * 驗證token是否還有效
 *
 * @param token       客戶端傳入的token
 * @param userDetails 從數據庫中查詢出來的用戶信息
 */
 public boolean validateToken(String token, UserDetails userDetails) {
 String username = getUserNameFromToken(token);
 return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
 }
​
 /**
 * 判斷token是否已經失效
 */
 private boolean isTokenExpired(String token) {
 Date expiredDate = getExpiredDateFromToken(token);
 return expiredDate.before(new Date());
 }
​
 /**
 * 從token中獲取過時時間
 */
 private Date getExpiredDateFromToken(String token) {
 Claims claims = getClaimsFromToken(token);
 return claims.getExpiration();
 }
​
 /**
 * 根據用戶信息生成token
 */
 public String generateToken(UserDetails userDetails) {
 Map<String, Object> claims = new HashMap<>();
 claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
 claims.put(CLAIM_KEY_CREATED, new Date());
 return generateToken(claims);
 }
​
 /**
 * 當原來的token沒過時時是能夠刷新的
 *
 * @param oldToken 帶tokenHead的token
 */
 public String refreshHeadToken(String oldToken) {
 if(StrUtil.isEmpty(oldToken)){
 return null;
 }
 String token = oldToken.substring(tokenHead.length());
 if(StrUtil.isEmpty(token)){
 return null;
 }
 //token校驗不經過
 Claims claims = getClaimsFromToken(token);
 if(claims==null){
 return null;
 }
 //若是token已通過期,不支持刷新
 if(isTokenExpired(token)){
 return null;
 }
 //若是token在30分鐘以內剛刷新過,返回原token
 if(tokenRefreshJustBefore(token,30*60)){
 return token;
 }else{
 claims.put(CLAIM_KEY_CREATED, new Date());
 return generateToken(claims);
 }
 }
​
 /**
 * 判斷token在指定時間內是否剛剛刷新過
 * @param token 原token
 * @param time 指定時間(秒)
 */
 private boolean tokenRefreshJustBefore(String token, int time) {
 Claims claims = getClaimsFromToken(token);
 Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
 Date refreshDate = new Date();
 //刷新時間在建立時間的指定時間內
 if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
 return true;
 }
 return false;
 }
}

jwt 過濾器:

package com.ssk.shop.config.security;
​
import com.ssk.shop.config.security.token.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
​
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * jwt 過濾器
 *
 * @author ssk
 * @since 2020-11-03 08:09:00
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
 @Resource
 private UserDetailsService userDetailsService;
 @Autowired
 private JwtTokenUtil jwtTokenUtil;
 @Value("${jwt.tokenHeader}")
 private String tokenHeader;
 @Value("${jwt.tokenHead}")
 private String tokenHead;
​
 @Override
 protected void doFilterInternal(HttpServletRequest request,
 HttpServletResponse response,
 FilterChain chain) throws ServletException, IOException {
 String authHeader = request.getHeader(this.tokenHeader);
​
 if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
 String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
 String username = jwtTokenUtil.getUserNameFromToken(authToken);
 LOGGER.info("checking username:{}", username);
 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 if (jwtTokenUtil.validateToken(authToken, userDetails)) {
 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
 LOGGER.info("authenticated user:{}", username);
 SecurityContextHolder.getContext().setAuthentication(authentication);
 }
 }
 }
 chain.doFilter(request, response);
 }
}

此處若是使用的是單項目多類型用戶,且都想進行登陸控制,(admin可能涉及到不少的角色和權限控制,而用戶端並不會設計,而是採用單用戶角色)能夠將admin和user(其餘用戶)都繼承security的UserDetailsService,而後登陸以後的UserDetail進行從新封裝,最好是能放入角色,而後jwt驗證的時候根據id拿出Userdetail信息中的角色來進行相對應的層從新受權,實例代碼:

//將繼承了UserDetail的類都注入
 @Resource
 private IAdminInfoFacade adminDetailsService;
 @Resource
 private IUserInfoFacade userDetailsFacade;
 
​
​
​
 @Override
 protected void doFilterInternal(HttpServletRequest request,
 HttpServletResponse response,
 FilterChain chain) throws ServletException, IOException {
 String authHeader = request.getHeader(this.tokenHeader);
 if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
 String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
 if (jwtTokenUtil.validateTokenSingle(authToken)){
 String username = jwtTokenUtil.getUserNameFromToken(authToken);
 //根據token解析完以後拿到角色 ADMIN/USER
 String role = jwtTokenUtil.getUserRoleFromToken(authToken);
 LOGGER.info("checking username:{}", username);
 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
 UserDetails userDetails = null;
 //更具角色來選擇進入那個實現類進行從新受權校驗
 if (role.equals(UserRoleTypeEnum.ADMIN.getType()) || role.equals(UserRoleTypeEnum.SUPER.getType())){
 userDetails = this.adminDetailsService.loadUserByUsername(username);
 }else if (role.equals(UserRoleTypeEnum.USER.getType())){
 userDetails = this.userDetailsFacade.loadUserByUsername(username);
 }
 if (jwtTokenUtil.validateToken(authToken, userDetails)) {
 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
 LOGGER.info("authenticated user:{}", username);
 SecurityContextHolder.getContext().setAuthentication(authentication);
 System.out.println("//");
 }
 }
 }
 }

可是這種方法處理建議不去使用security自帶的登陸,而是採用每種角色進行登陸接口的實現。

自定義未受權返回:

package com.ssk.shop.config.security.token;
​
import cn.hutool.json.JSONUtil;
import com.ssk.utils.CommonResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * 自定義返回結果:沒有權限訪問時
 * @author ssk
 * @since 2020-11-03 08:08:40
 */
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
 @Override
 public void handle(HttpServletRequest request,
 HttpServletResponse response,
 AccessDeniedException e) throws IOException, ServletException {
 response.setHeader("Access-Control-Allow-Origin", "*");
 response.setHeader("Cache-Control","no-cache");
 response.setCharacterEncoding("UTF-8");
 response.setContentType("application/json");
 response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
 response.getWriter().flush();
 }
}

自定義未登陸/未認證返回:

package com.ssk.shop.config.security.token;
​
import cn.hutool.json.JSONUtil;
import com.ssk.utils.CommonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * 自定義返回結果:未登陸或登陸過時
 * @author ssk
 * @since 2020-11-03 08:08:40
 */
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
 @Override
 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
 response.setHeader("Access-Control-Allow-Origin", "*");
 response.setHeader("Cache-Control","no-cache");
 response.setCharacterEncoding("UTF-8");
 response.setContentType("application/json");
 response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
 response.getWriter().flush();
 }
}

jwt 的基礎配置

jwt:
 tokenHeader: Authorization #JWT存儲的請求頭
 secret: jwt_key #JWT加解密使用的密鑰
 expiration: 604800 #JWT的超期限時間(60*60*24)
 tokenHead: Bearer  #JWT負載中拿到開頭
 
#security不用受權的放行名單  須要結合上述的security配置文件
secure:
 ignored:
 urls: #安全路徑白名單
 - /swagger-ui.html
 - /swagger-resources/**
 - /swagger/**
 - /**/v2/api-docs
 - /webjars/springfox-swagger-ui/**
 - /actuator/**
 - /druid/**
 - /admin-info/login
 - /admin/register
 - /admin/info
 - /admin/logout

以上即是全部的基礎配置,jwt的密鑰以及頭我是直接放入配置文件中。

五、登陸測試

image

此處使用swagger直接進行測試,使用的登陸方法是本身進行重寫

@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
​
@ApiOperation(value = "登陸之後返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public CommonResult<Map<String, String>> login(@RequestBody UserLoginVO user) {
 try {
 String token = adminInfoFacade.login(user.getUsername(), user.getPassword());
 Map<String, String> tokenMap = new HashMap<>();
 tokenMap.put("token", token);
 tokenMap.put("tokenHead", tokenHead);
 return CommonResult.success(tokenMap);
 }catch (ServiceException e){
 return CommonResult.failed(e.getMessage());
 }
}

將security的loadUserByUsername進行重寫,而後自定義一個登錄類LoginMan用於保存帳號信息:

private PasswordEncoder passwordEncoder;
​
 @Resource
 private JwtTokenUtil jwtTokenUtil;
​
 @Override
 public String login(String username, String password) {
 String token = null;
 try {
 LoginMan userDetails = this.loadUserByUsername(username);
 if(!passwordEncoder.matches(password,userDetails.getPassword())){
 throw new BadCredentialsException("密碼不正確");
 }
 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 SecurityContextHolder.getContext().setAuthentication(authentication);
 token = jwtTokenUtil.generateToken(userDetails);
 } catch (AuthenticationException e) {
 LOGGER.warn("登陸異常:{}", e.getMessage());
 throw new ServiceException("用戶名或密碼錯誤");
 }
 return token;
 }

LoginMan:根據本身的業務需求進行改寫,主要實現對應的接口:

package com.ssk.shop.dto;
​
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * 登陸用戶實體
 *
 * @author ssk
 * @since 2020-11-03 08:09:00
 */
public class LoginMan implements GrantedAuthority, UserDetails {
 /**
 * 實體id
 */
 private String id;
​
​
 /**
 * 密碼
 */
 private String password;
​
 private Object manMsg;
​
 private String roleType;
​
 /**
 * 名稱
 */
 private String name;
​
 private List<String> permissions;
​
 private boolean enabled = false;
​
 public Object getManMsg() {
 return manMsg;
 }
​
 public void setManMsg(Object manMsg) {
 this.manMsg = manMsg;
 }
​
 @Override
 @JsonIgnore
 public boolean isAccountNonExpired() { // 賬戶是否過時
 return true;
 }
​
 @Override
 @JsonIgnore
 public boolean isAccountNonLocked() { // 賬戶是否被凍結
 return true;
 }
​
 // 賬戶密碼是否過時,通常有的密碼要求性高的系統會使用到,比較每隔一段時間就要求用戶重置密碼
 @Override
 @JsonIgnore
 public boolean isCredentialsNonExpired() {
 return true;
 }
​
 @Override
 public boolean isEnabled() {
 return enabled;
 }
​
 public void setPassword(String password) {
 this.password = password;
 }
​
 public String getName() {
 return name;
 }
​
 public void setName(String name) {
 this.name = name;
 }
​
 public void setEnabled(boolean enabled) {
 this.enabled = enabled;
 }
​
 public String getRoleType() {
 return roleType;
 }
​
 public void setRoleType(String roleType) {
 this.roleType = roleType;
 }
​
 public List<String> getPermissions() {
 return permissions;
 }
​
 public String getId() {
 return id;
 }
​
 public void setId(String id) {
 this.id = id;
 }
​
​
 @Override
 @JsonIgnore
 public List<GrantedAuthority> getAuthorities() {
 List<GrantedAuthority> authorities = new ArrayList<>();
 authorities.add(new SimpleGrantedAuthority("ROLE_" + roleType));
 for (String permission : permissions) {
 authorities.add(new SimpleGrantedAuthority(permission));
 }
 return authorities;
 }
​
 @Override
 public String getPassword() {
 return password;
 }
​
 @Override
 public String getUsername() {
 return name;
 }
​
 @Override
 @JsonIgnore
 public String getAuthority() {
 return null;
 }
​
 public LoginMan(String name, String password, String roleType, List<String> permissions, String id , Object manMsg) {
 this.name = name;
 this.password = password;
 this.roleType = roleType;
 this.permissions = permissions;
 this.id = id;
 this.manMsg = manMsg;
 }
​
 public LoginMan() {
​
 }
​
 @Override
 public String toString() {
 return "LoginMan{" +
 "id='" + id + ''' +
 ", password='" + password + ''' +
 ", manMsg=" + manMsg +
 ", roleType='" + roleType + ''' +
 ", name='" + name + ''' +
 ", permissions=" + permissions +
 ", enabled=" + enabled +
 '}';
 }
}

登陸:

#成功
{
 "code": 200,
 "message": "操做成功",
 "data": {
 "tokenHead": "Bearer",
 "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmcmlkYXkiLCJjcmVhdGVkIjoxNjA0NzA5NzkyMjI2LCJleHAiOjE2MDUzMTQ1OTJ9.EHoNsN1W1Q3sek64XGV0ubIXhnTBpcvjAezbzDOLQAhQnCzvO9f4ovCJUvtrLyzSexqvJcoOR8ccBEPd6HXFOw"
 }
}

#失敗
{
 "code": 500,
 "message": "用戶名或密碼錯誤",
 "data": null
}

image

而後將token給swagger全局(token須要加上頭,且與值之間用空格);

對須要權限的接口進行訪問:

@PreAuthorize("hasAuthority('ADMIN_SEE')")
@RequestMapping("/query_page")
@ApiOperation(value = "查詢管理員集合", notes = "查詢管理員集合", httpMethod = "GET")
@ApiImplicitParams({
 @ApiImplicitParam(name = "roleId", value = "角色ID", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "account", value = "帳號", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "name", value = "名字", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "sex", value = "性別(0:男,1:女)", paramType = "query", dataType = "int"),
 @ApiImplicitParam(name = "enable", value = "狀態(1是啓用,0是禁用))", paramType = "query", dataType = "int"),
 @ApiImplicitParam(name = "phone", value = "電話", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "page", value = "展現的頁數", required = true, paramType = "query", dataType = "int",example = "1"),
 @ApiImplicitParam(name = "pageSize", value = "每頁展現幾條", required = true, paramType = "query", dataType = "int",example = "10")
})
public CommonResult<CommonPage<AdminListDto>> selectAdmin(@RequestParam(required = false) String roleId,
 @RequestParam(required = false) String account,
 @RequestParam(required = false) String name,
 @RequestParam(required = false) Integer sex,
 @RequestParam(required = false) Integer enable,
 @RequestParam(required = false) String phone,
 Integer page, Integer pageSize) {
 try {
 return CommonResult.success(adminInfoFacade.selectAdmin(roleId, account, name, sex, enable, phone,  page, pageSize));
 } catch (ServiceException e) {
 e.printStackTrace();
 return CommonResult.failed(e.getMessage());
 }
}

成功獲取返回值:

{
 "code": 200,
 "message": "操做成功",
 "data": {
 "total": 0,
 "size": 10,
 "pages": 0,
 "current": 1,
 "records": [
 {
 "adminInfoId": "053d9349b5ca4c56a873053c2afbc6df",
 "account": "bjty",
 "password": "$2a$10$jSw6APBmhUzkNuSVFKEGR.yu/srrbwHUimpvllm1QZB9ifMf1wXry",
 "adminRole": "fcc5a1585f5b4e32bc283a3e232435c0",
 "roleName": "管理員2號",
 "adminDepartmentName": null,
 "name": "白開水",
 "sex": null,
 "phone": "18999999999",
 "enableState": 1,
 "createTime": "2020-08-25T02:28:47.000+0000"
 },

清空token後進行訪問:

{
 "code": 401,
 "data": "Full authentication is required to access this resource",
 "message": "暫未登陸或token已通過期"
}

總結:

項目中初期設定是後端管理員能夠擁有單角色,而後給角色進行權限分類,接口訪問經過權限或角色進行控制。我的的理解是security將帳號的權限以及角色在登陸時放入了session(啓用的狀況下),而後經過會話訪問時獲取session中的會話信息,從而拿到權限進行比較,知足了即可放行訪問。

security經過aop的方式進行權限校驗,經過註解進行權限比較

@PreAuthorize("hasAnyRole('ROLE_ADMIN')")//角色

@PreAuthorize("hasAuthority('ADMIN_SEE')")//權限

其中對於角色,咱們須要在登陸時將角色初始化如權限集合時添加上「ROLE_」,用以標識角色。

public LoginMan(String name, String password, String roleType, List<String> permissions, String id , Object manMsg) {
 this.name = name;
 this.password = password;
 this.roleType = roleType;
 this.permissions = permissions;
 this.id = id;
 this.manMsg = manMsg;
}
​
@Override
@JsonIgnore
public List<GrantedAuthority> getAuthorities() {
 List<GrantedAuthority> authorities = new ArrayList<>();
 authorities.add(new SimpleGrantedAuthority("ROLE_" + roleType));
 for (String permission : permissions) {
 authorities.add(new SimpleGrantedAuthority(permission));
 }
 return authorities;
}

本文中實例化LoginMan(UserDetail)是將角色類型單獨做爲參數傳入。

若是未禁用session,還能夠經過獲取下列方式獲取session中的值:

/**
 * 獲取當前帳戶id
 * @return
 * @throws ServiceException
 */
public String getThisAccountId()throws ServiceException {
 try {
 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
 if (ObjectHelper.isNotEmpty(principal) && principal.equals("anonymousUser")){
 return null;
 }else {
 LoginMan thisMan = (LoginMan) principal;
 return thisMan.getId();
 }
 }catch (Exception e){
 return null;
 }
}

後期將會禁用session,採用token保存的方式進行實現。

相關文章
相關標籤/搜索