technology-integration(七)---使用SpringSecurity做JWT認證授權
SpringSecurity是什麼
Spring Security是一個能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面程式設計)功能,為應用系統提供宣告式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作。(引用百度百科)
為什麼使用SpringSecurity
可以說SpringBoot的快速發展也使得了SpringSecurity的熱度往上漲,在SpringBoot以前,Shiro和SpringSecurity的框架都是主流的安全框架,但Security的配置非常臃腫,效能也相對慢一些,最大的缺點還是必須依賴於Spring框架才能開發,不過唯一值得一提的就是Security預設實現了更多功能,更是提供了oauth授權的實現。不過一切的改變還是源於SpringBoot的出現,SpringBoot整合SpringSecurity的步驟非常簡單,只需要繼承WebSecurityConfigurerAdapter這個類並實現認證方法,接著配置一下登入的uri即可完成一個簡單的使用者認證功能,可以說整個功能都不用5分鐘就能實現。我個人還是比較喜歡SpringBoot全家桶,不需要解決框架整合上的小麻煩,功能上來說也很強大。
JWT
JWT是一種用於雙方之間傳遞安全資訊的簡潔的、URL安全的表述性宣告規範。JWT作為一個開放的標準(RFC 7519),定義了一種簡潔的,自包含的方法用於通訊雙方之間以Json物件的形式安全的傳遞資訊。因為數字簽名的存在,這些資訊是可信的,JWT可以使用HMAC演算法或者是RSA的公私祕鑰對進行簽名。簡潔(Compact): 可以通過URL,POST引數或者在HTTP header傳送,因為資料量小,傳輸速度也很快 自包含(Self-contained):負載中包含了所有使用者所需要的資訊,避免了多次查詢資料庫
怎麼使用
使用JWT,我們只需要在請求的請求頭上新增如圖下類似的資料(token)。後端根據需要認證的url進行攔截,取出Hearders裡面的資料,緊接著解析出這段token的包含的資訊,判斷資訊是否正確即可。token其實就是根據資訊加密而來的一段字串,我們將需要用到的資訊放到token中,token包含的資訊儘可能的簡潔。
注意:
雖然簡單的jwt認證並沒有什麼難度,但如果你沒使用過SpringSecurity,建議還是先去簡單的學習一下。
開始
- 編寫通過使用者id或使用者手機號碼查詢User和Role的方法
- 編寫Token生成工具類
- 繼承UserDetails介面
- 繼承UserDetailsService介面,實現使用者認證方法
- 編寫使用者賬號驗證失敗處理器與許可權不足處理器
- 編寫Token驗證過濾器
- 配置SpringSecurity Config
- 實現登入方法
整個流程還是相對完善的,所以步驟稍多
匯入jar
這裡需要提一下的就是,當你引入這個包的時候,SpringBoot預設會為專案所有的請求新增認證,這也是SpringSecurity的常規操作,如果你還不知道的話,趕快剎車調頭回家補課。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
實現使用者登入方法
使用者通過手機號及密碼進行登入,我們需要先獲取使用者的身份資訊以及角色資訊
UserMapper.xml
<resultMap id="User_Role" type="com.viu.technology.po.User">
<id property="id" column="id" javaType="java.lang.String" jdbcType="BIGINT" />
<result property="name" column="name" javaType="java.lang.String" jdbcType="VARCHAR" />
<result property="phone" column="phone" javaType="java.lang.String" jdbcType="VARCHAR" />
<result property="password" column="password" javaType="java.lang.String" jdbcType="VARCHAR" />
<collection property="roles" ofType="com.viu.technology.po.Role">
<id property="id" column="role_id" jdbcType="BIGINT"/>
<result property="roleName" column="role_name" jdbcType="VARCHAR" />
</collection>
</resultMap>
<select id="selUserAndRoleByPhone" parameterType="java.lang.String" resultMap="User_Role">
SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
from t_user u
LEFT JOIN t_role r on u.id=r.user_id
where u.phone=#{phone,jdbcType=VARCHAR}
</select>
<select id="selUserAndRoleById" parameterType="java.lang.String" resultMap="User_Role">
SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
from t_user u
LEFT JOIN t_role r on u.id=r.user_id
where u.id=#{id,jdbcType=VARCHAR}
</select>
UserMapper.java
User selUserAndRoleByPhone(String phone);
User selUserAndRoleById(String id);
UserDao.java
User selUserAndRoleByPhone(String phone);
User selUserAndRoleById(String id);
UserDaoImpl.java
public User selUserAndRoleByPhone(String phone) {
User user = userMapper.selUserAndRoleByPhone(phone);
return user;
}
public User selUserAndRoleById(String id){
User user = userMapper.selUserAndRoleById(id);
return user;
}
UserService.java
User getUserAndRoleByPhone(String phone);
User getUserAndRoleById(String id);
UserServiceImpl.java
public User getUserAndRoleByPhone(String phone) {
User user = userDao.selUserAndRoleByPhone(phone);
return user;
}
public User getUserAndRoleById(String id) {
User user = userDao.selUserAndRoleById(id);
return user;
}
運算元據庫獲取使用者身份資訊的程式碼就到此為止了,接下來就開始編寫SpringSecurity+jwt的認證程式碼了
編寫Token生成工具類----JwtTokenUtil
工具類主要用作生成token、重新整理token以及驗證token。Token和Session一個很大的區別就是無登入狀態,我們可以利用清除session做登出的操作,但無法利用token直接做登出操作,後續會進行講解。
這個token裡的資訊比較簡單,只存放了sub和create,你可以根據自己業務需求在generateToken(UserDetails userDetails)方法裡面新增不同的資料即可,後續通過getClaimsFromToken方法獲取Claims物件,接著呼叫Claims物件的get方法獲取出對應的資料即可。
@Component
public class JwtTokenUtil{
/**
* 金鑰
*/
private static final String secret = "lkhouhubkljgpihojblkjboiboihu9u";
/**
* 從資料宣告生成令牌
*
* @param claims 資料宣告
* @return 令牌
*/
public static String generateToken(Map<String, Object> claims) {
//設定token的有效期為24*7小時,也就是一週
Date expirationDate = new Date(System.currentTimeMillis() +60*60*24*7 * 1000);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 從令牌中獲取資料宣告
*
* @param token 令牌
* @return 資料宣告
*/
public static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 使用者
* @return 令牌
*/
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 從令牌中獲取使用者名稱
*
* @param token 令牌
* @return 使用者名稱
*/
public static String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判斷令牌是否過期
*
* @param token 令牌
* @return 是否過期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 重新整理令牌
*
* @param token 原令牌
* @return 新令牌
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 驗證令牌
*
* @param token 令牌
* @param userDetails 使用者
* @return 是否有效
*/
public static Boolean validateToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
繼承UserDetails介面
UserDetails介面是SpringSecurity框架用於認證授權的一個載體,只有實現了這個介面的類才能被SpringSecurity驗證,
public class User implements UserDetails {
private String id;
private String name;
private String password;
private String phone;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public User(String id, String name, String password, String phone) {
this.id = id;
this.name = name;
this.password = password;
this.phone = phone;
}
public User() {
super();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//獲取使用者角色許可權,此處從資料庫表Role中獲取
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
List<Role> roles = getRoles();
if (roles!=null) {
for (Role role : roles) {
auths.add(new SimpleGrantedAuthority(role.getRoleName()));
}
}
return auths;
}
//這個是UserDetails預設實現獲取密碼的方法
@Override
public String getPassword() {
return password;
}
//這裡getUsername翻譯過來就是獲取使用者名稱的意思,但這個可以作為我們獲取使用者資訊的一個標識
@Override
public String getUsername() {
return id;
}
//使用者賬號是否過期,暫時沒這個功能,預設返回true,即未過期
@Override
public boolean isAccountNonExpired() {
return true;
}
//使用者賬號是否鎖定
@Override
public boolean isAccountNonLocked() {
return true;
}
//使用者憑證是否過期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//賬號是否可用
@Override
public boolean isEnabled() {
return true;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
編寫登入認證方法JwtUserDetailsServiceImpl.java
該類位於com.viu.technology.service.auth包下(自行建包)
JwtUserDetailsServiceImpl實現了UserDetailsService介面,SpringSecurity會去IOC容器中尋找實現這個介面的實現類,並將該實現類作為預設的認證類。這個類主要用於獲取使用者身份資訊,並不需要我們去判斷使用者名稱和密碼是否匹配。參照UserDetails實現的getPassword和getUsername方法。
這裡之所要對username的長度進行判斷是因為,我們登入的時候用的是手機號+明文密碼進行登入,而儲存在token裡的資訊只有id。登入方法和Token認證過濾器都會呼叫loadUserByUsername方法,所以需要做一個判斷。可能會有一點疑問,既然是這樣,為什麼不直接用手機號做為token的傳遞資訊就好了呢,主要還是因為我們使用手機號查詢的情況比較少,而表的主鍵id才是經常用的。
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
@Lazy
private UserService userService;
public JwtUserDetailsServiceImpl(){
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = null;
if (username.length() == 32) {
user= userService.getUserAndRoleById(username);
} else if(username.length()==11) {
user= userService.getUserAndRoleByPhone(username);
}
log.info("user:" + user);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
}else{
return user;
}
}
}
編寫賬號密碼驗證失敗處理器EntryPointUnauthorizedHandler.java
位於com.viu.technology.handler包下,自行建立
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
private static Logger log = LoggerFactory.getLogger(EntryPointUnauthorizedHandler.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_LOGIN_FIAL)));
}
}
編寫賬戶許可權不足處理器RestAccessDeniedHandler.java
位於com.viu.technology.handler包下,自行建立
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(403);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_PERMISSION_DENIED)));
}
}
編寫Token驗證過濾器JwtAuthenticationTokenFilter.java
位於com.viu.technology.filter包下,自行建立
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
//該字串作為Authorization請求頭的值的字首
String tokenHead = "tech-";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
//從token中獲取userId
String userId = JwtTokenUtil.getUsernameFromToken(authToken);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//呼叫UserDetailsService的認證方法(JwtUserDetailsServiceImpl實現類)
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);
//驗證token是否正確
if (JwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//將獲取到的使用者身份資訊放到SecurityContextHolder中,這個類是為了線上程中儲存當前使用者的身份資訊
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} else {
log.info("沒有獲取到token");
}
chain.doFilter(request, response);
}
}
配置SpringSecurity
位於com.viu.technology.config.security包下,自行建立
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟security方法級別許可權控制註解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SimpleUrlAuthenticationSuccessHandler successHandler;
@Autowired
private SimpleUrlAuthenticationFailureHandler failureHandler;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//這裡的引數為不需要認證的uri,**代表匹配多級路徑,*代表匹配一級路徑,#代表一個字元....
.antMatchers(
"/demo/**",
"/user/generate/token"
).permitAll()
//這裡表示該路徑需要管理員角色
.antMatchers("/auth/test").hasAnyAuthority("管理員")
.anyRequest().authenticated()
.and()
.headers().cacheControl();
//新增認證過濾
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//新增許可權不足及驗證失敗處理器
httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);
}
//這個為SpringSecurity的加密類
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
實現登入方法
UserService.java
String login(String phone, String password);
UserServiceImpl.java
這裡需要注意一下,UsernamePasswordAuthenticationToken會自動將password進行加密之後再比對,而我們之前寫的註冊使用者方法是以明文方式存入資料庫的,並沒有加密,所以我們需要修改一下使用者註冊方法,然後重新註冊
public String login(String phone, String password) {
//將使用者名稱和密碼生成Token
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(phone, password);
//呼叫該方法時SpringSecurity會去呼叫JwtUserDetailsServiceImpl 進行驗證
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return JwtTokenUtil.generateToken(userDetails);
}
@Autowired
PasswordEncoder passwordEncoder;
public User registerUser(User user) {
//在插入資料庫時將原密碼進行加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
User userRes = userDao.insertUser(user);
Role roleRes = roleDao.insertRole(new Role("普通群眾", user.getId()));
List list = new ArrayList();
list.add(roleRes);
if (null != userRes && null != roleRes) {
userRes.setRoles(list);
return user;
}
return null;
}
UserController.java
@PostMapping(value = "/generate/token")
public Result getToken(String phone, String password) throws AuthenticationException {
String token = userService.login(phone, password);
return Result.success(token);
}
測試獲取token介面
接著我們呼叫一下之前寫的註冊介面,發現沒發註冊,因為我們在SpringSecurity的配置中並沒有開放這個介面的認證,自行新增。註冊是不需要使用者身份驗證的,否則你讓人家怎麼註冊嘛。。。
測試Token是否能正常使用
UserController.java
@GetMapping("/self/info")
public Result getUserSelfInfo() {
//由於通過驗證後我們會把使用者物件存到SecurityContextHolder中,所以這時候我們能通過下面這句程式碼獲取到使用者的身份資訊
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return Result.success(user);
}
接下來測試一下,如果能夠正常獲取就代表成功,記住token前面要加tech-這個幾個字串,看不順眼的話自己去改過濾器
溫馨提醒
你們會發現讀出來的資料和我稍微有點不一樣對吧,哈哈哈哈,肯定啊,你們沒有過濾一下敏感欄位(密碼我忘了過濾了0.0),在User類上加入@JSONField(serialize = false)註解即可,SpringBoot會將持有該註解的欄位過濾不進行輸出。
更多文章請關注該 technology-integration全面解析專題
相關文章
- SpringSecurity認證和授權流程詳解SpringGse
- SpringBoot--- 使用SpringSecurity進行授權認證Spring BootGse
- 【ASP.NET Core學習】使用JWT認證授權ASP.NETJWT
- SpringSecurity(1)---認證+授權程式碼實現SpringGse
- Dotnet core使用JWT認證授權最佳實踐(一)JWT
- Dotnet core使用JWT認證授權最佳實踐(二)JWT
- Spring Security OAuth2.0認證授權三:使用JWT令牌SpringOAuthJWT
- ASP.NET Core 6.0 新增 JWT 認證和授權ASP.NETJWT
- EMQX Cloud 更新:新增 Redis 和 JWT 外部認證授權MQCloudRedisJWT
- 認證授權
- 登入模組 使用者認證 SpringSecurity +Oauth2+JwtSpringGseOAuthJWT
- 認證授權方案之授權初識
- SpringSecurity之授權SpringGse
- 認證授權方案之JwtBearer認證JWT
- 認證授權方案之授權揭祕 (上篇)
- .Net Core官方的 JWT 授權驗證JWT
- 【認證與授權】Spring Security的授權流程Spring
- Ocelot(四)- 認證與授權
- OAuth 2.0 授權碼認證OAuth
- puppet自動認證授權
- 授權(Authorization)和認證(Authentication)
- Ceph配置與認證授權
- Spring Security OAuth2.0認證授權四:分散式系統認證授權SpringOAuth分散式
- asp.net core 3.1多種身份驗證方案,cookie和jwt混合認證授權ASP.NETCookieJWT
- Spring Security OAuth2.0認證授權五:使用者資訊擴充套件到jwtSpringOAuth套件JWT
- 認證授權問題概覽
- OAuth 2.0 授權認證詳解OAuth
- EMQX Cloud 更新:外部認證授權MQCloud
- shiro授權和認證(四)
- 認證授權:IdentityServer4IDEServer
- 認證授權:學習OIDC
- 安全測試之認證授權
- 細說API - 認證、授權和憑證API
- 【認證與授權】2、基於session的認證方式Session
- SpringSecurity認證流程SpringGse
- SpringSecurity之認證SpringGse
- 使用 JWT 認證使用者身份JWT
- 認證/授權與許可權的問題