mall整合SpringSecurity及JWT實(shí)現(xiàn)認(rèn)證授權(quán)實(shí)戰(zhàn)
摘要
本文主要講解mall通過整合SpringSecurity和JWT實(shí)現(xiàn)后臺(tái)用戶的登錄和授權(quán)功能,同時(shí)改造Swagger-UI的配置使其可以自動(dòng)記住登錄令牌進(jìn)行發(fā)送。
項(xiàng)目使用框架介紹
SpringSecurity
SpringSecurity是一個(gè)強(qiáng)大的可高度定制的認(rèn)證和授權(quán)框架,對(duì)于Spring應(yīng)用來(lái)說(shuō)它是一套Web安全標(biāo)準(zhǔn)。SpringSecurity注重于為Java應(yīng)用提供認(rèn)證和授權(quán)功能,像所有的Spring項(xiàng)目一樣,它對(duì)自定義需求具有強(qiáng)大的擴(kuò)展性。
JWT
JWT是JSON WEB TOKEN的縮寫,它是基于 RFC 7519 標(biāo)準(zhǔn)定義的一種可以安全傳輸?shù)牡腏SON對(duì)象,由于使用了數(shù)字簽名,所以是可信任和安全的。
JWT的組成
JWT token的格式:
header.payload.signature
header中用于存放簽名的生成算法
{"alg": "HS512"}
payload中用于存放用戶名、token的生成時(shí)間和過期時(shí)間
{"sub":"admin","created":1489079981393,"exp":1489684781}
signature為以header和payload生成的簽名,一旦header和payload被篡改,驗(yàn)證將失敗
//secret為加密算法的密鑰 String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
JWT實(shí)例
這是一個(gè)JWT的字符串
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw
可以在該網(wǎng)站上獲得解析結(jié)果:jwt.io/
JWT實(shí)現(xiàn)認(rèn)證和授權(quán)的原理
- 用戶調(diào)用登錄接口,登錄成功后獲取到JWT的token;
- 之后用戶每次調(diào)用接口都在http的header中添加一個(gè)叫Authorization的頭,值為JWT的token;
- 后臺(tái)程序通過對(duì)Authorization頭中信息的解碼及數(shù)字簽名校驗(yàn)來(lái)獲取其中的用戶信息,從而實(shí)現(xiàn)認(rèn)證和授權(quán)。
Hutool
Hutool是一個(gè)豐富的Java開源工具包,它幫助我們簡(jiǎn)化每一行代碼,減少每一個(gè)方法,mall項(xiàng)目采用了此工具包。
項(xiàng)目使用表說(shuō)明
ums_admin
:后臺(tái)用戶表
ums_role
:后臺(tái)用戶角色表
ums_permission
:后臺(tái)用戶權(quán)限表
ums_admin_role_relation
:后臺(tái)用戶和角色關(guān)系表,用戶與角色是多對(duì)多關(guān)系
ums_role_permission_relation
:后臺(tái)用戶角色和權(quán)限關(guān)系表,角色與權(quán)限是多對(duì)多關(guān)系
ums_admin_permission_relation
:后臺(tái)用戶和權(quán)限關(guān)系表(除角色中定義的權(quán)限以外的加減權(quán)限),加權(quán)限是指用戶比角色多出的權(quán)限,減權(quán)限是指用戶比角色少的權(quán)限
整合SpringSecurity及JWT
在pom.xml中添加項(xiàng)目依賴
<!--SpringSecurity依賴配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--Hutool Java工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.5.7</version> </dependency> <!--JWT(Json Web Token)登錄支持--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
添加JWT token的工具類
用于生成和解析JWT token的工具類
相關(guān)方法說(shuō)明:
generateToken(UserDetails userDetails) :用于根據(jù)登錄用戶信息生成token
getUserNameFromToken(String token):從token中獲取登錄用戶的信息
validateToken(String token, UserDetails userDetails):判斷token是否還有效
package com.macro.mall.tiny.common.utils; 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 org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JwtToken生成的工具類 * Created by macro on 2018/4/26. */ @Component 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; /** * 根據(jù)負(fù)責(zé)生成JWT的token */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 從token中獲取JWT中的負(fù)載 */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式驗(yàn)證失敗:{}",token); } return claims; } /** * 生成token的過期時(shí)間 */ 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; } /** * 驗(yàn)證token是否還有效 * * @param token 客戶端傳入的token * @param userDetails 從數(shù)據(jù)庫(kù)中查詢出來(lái)的用戶信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判斷token是否已經(jīng)失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } /** * 從token中獲取過期時(shí)間 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 根據(jù)用戶信息生成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是否可以被刷新 */ public boolean canRefresh(String token) { return !isTokenExpired(token); } /** * 刷新token */ public String refreshToken(String token) { Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } }
添加SpringSecurity的配置類
package com.macro.mall.tiny.config; import com.macro.mall.tiny.component.JwtAuthenticationTokenFilter; import com.macro.mall.tiny.component.RestAuthenticationEntryPoint; import com.macro.mall.tiny.component.RestfulAccessDeniedHandler; import com.macro.mall.tiny.dto.AdminUserDetails; import com.macro.mall.tiny.mbg.model.UmsAdmin; import com.macro.mall.tiny.mbg.model.UmsPermission; import com.macro.mall.tiny.service.UmsAdminService; 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.authentication.AuthenticationManager; 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.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.util.List; /** * SpringSecurity的配置 * Created by macro on 2018/4/26. */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UmsAdminService adminService; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf()// 由于使用的是JWT,我們這里不需要csrf .disable() .sessionManagement()// 基于token,所以不需要session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, // 允許對(duì)于網(wǎng)站靜態(tài)資源的無(wú)授權(quán)訪問 "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-resources/**", "/v2/api-docs/**" ) .permitAll() .antMatchers("/admin/login", "/admin/register")// 對(duì)登錄注冊(cè)要允許匿名訪問 .permitAll() .antMatchers(HttpMethod.OPTIONS)//跨域請(qǐng)求會(huì)先進(jìn)行一次options請(qǐng)求 .permitAll() // .antMatchers("/**")//測(cè)試時(shí)全部運(yùn)行訪問 // .permitAll() .anyRequest()// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證 .authenticated(); // 禁用緩存 httpSecurity.headers().cacheControl(); // 添加JWT filter httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定義未授權(quán)和未登錄結(jié)果返回 httpSecurity.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthenticationEntryPoint); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService() { //獲取登錄用戶信息 return username -> { UmsAdmin admin = adminService.getAdminByUsername(username); if (admin != null) { List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId()); return new AdminUserDetails(admin,permissionList); } throw new UsernameNotFoundException("用戶名或密碼錯(cuò)誤"); }; } @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
相關(guān)依賴及方法說(shuō)明
configure(HttpSecurity httpSecurity):用于配置需要攔截的url路徑、jwt過濾器及出異常后的處理器;
configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
RestfulAccessDeniedHandler:當(dāng)用戶沒有訪問權(quán)限時(shí)的處理器,用于返回JSON格式的處理結(jié)果;
RestAuthenticationEntryPoint:當(dāng)未登錄或token失效時(shí),返回JSON格式的結(jié)果;
UserDetailsService:SpringSecurity定義的核心接口,用于根據(jù)用戶名獲取用戶信息,需要自行實(shí)現(xiàn);
UserDetails:SpringSecurity定義用于封裝用戶信息的類(主要是用戶信息和權(quán)限),需要自行實(shí)現(xiàn);
PasswordEncoder:SpringSecurity定義的用于對(duì)密碼進(jìn)行編碼及比對(duì)的接口,目前使用的是BCryptPasswordEncoder;
JwtAuthenticationTokenFilter:在用戶名和密碼校驗(yàn)前添加的過濾器,如果有jwt的token,會(huì)自行根據(jù)token信息進(jìn)行登錄。
添加RestfulAccessDeniedHandler
package com.macro.mall.tiny.component; import cn.hutool.json.JSONUtil; import com.macro.mall.tiny.common.api.CommonResult; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 當(dāng)訪問接口沒有權(quán)限時(shí),自定義的返回結(jié)果 * Created by macro on 2018/4/26. */ @Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler{ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage()))); response.getWriter().flush(); } }
添加RestAuthenticationEntryPoint
package com.macro.mall.tiny.component; import cn.hutool.json.JSONUtil; import com.macro.mall.tiny.common.api.CommonResult; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 當(dāng)未登錄或者token失效訪問接口時(shí),自定義的返回結(jié)果 * Created by macro on 2018/5/14. */ @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage()))); response.getWriter().flush(); } }
添加AdminUserDetails
package com.macro.mall.tiny.dto; import com.macro.mall.tiny.mbg.model.UmsAdmin; import com.macro.mall.tiny.mbg.model.UmsPermission; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * SpringSecurity需要的用戶詳情 * Created by macro on 2018/4/26. */ public class AdminUserDetails implements UserDetails { private UmsAdmin umsAdmin; private List<UmsPermission> permissionList; public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) { this.umsAdmin = umsAdmin; this.permissionList = permissionList; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { //返回當(dāng)前用戶的權(quán)限 return permissionList.stream() .filter(permission -> permission.getValue()!=null) .map(permission ->new SimpleGrantedAuthority(permission.getValue())) .collect(Collectors.toList()); } @Override public String getPassword() { return umsAdmin.getPassword(); } @Override public String getUsername() { return umsAdmin.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return umsAdmin.getStatus().equals(1); } }
添加JwtAuthenticationTokenFilter
在用戶名和密碼校驗(yàn)前添加的過濾器,如果請(qǐng)求中有jwt的token且有效,會(huì)取出token中的用戶名,然后調(diào)用SpringSecurity的API進(jìn)行登錄操作。
package com.macro.mall.tiny.component; import com.macro.mall.tiny.common.utils.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.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * JWT登錄授權(quán)過濾器 * Created by macro on 2018/4/26. */ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); @Autowired 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); } }
項(xiàng)目源碼地址 github.com/macrozheng/…
mall持續(xù)更新地址:https://github.com/macrozheng/mall
以上就是mall整合SpringSecurity及JWT實(shí)現(xiàn)認(rèn)證授權(quán)實(shí)戰(zhàn)的詳細(xì)內(nèi)容,更多關(guān)于mall整合SpringSecurity JWT的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringSecurity+Redis+Jwt實(shí)現(xiàn)用戶認(rèn)證授權(quán)
- springboot+springsecurity+mybatis+JWT+Redis?實(shí)現(xiàn)前后端離實(shí)戰(zhàn)教程
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- SpringSecurity整合JWT的使用示例
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
- SpringSecurity詳解整合JWT實(shí)現(xiàn)全過程
- mall整合SpringSecurity及JWT認(rèn)證授權(quán)實(shí)戰(zhàn)下
- Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證
- springSecurity+jwt使用小結(jié)
相關(guān)文章
詳解springboot項(xiàng)目docker部署實(shí)踐
這篇文章主要介紹了詳解springboot項(xiàng)目docker部署實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2018-01-01SpringBoot 整合 Netty 多端口監(jiān)聽的操作方法
Netty提供異步的、基于事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用程序框架,用以快速開發(fā)高性能、高可靠性的網(wǎng)絡(luò) IO 程序,是目前最流行的 NIO 框架,這篇文章主要介紹了SpringBoot 整和 Netty 并監(jiān)聽多端口,需要的朋友可以參考下2023-10-10idea 創(chuàng)建properties配置文件的步驟
這篇文章主要介紹了idea 創(chuàng)建properties配置文件的步驟,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2021-01-01IDEA 中使用 ECJ 編譯出現(xiàn) java.lang.IllegalArgumentException的錯(cuò)誤問題
這篇文章主要介紹了IDEA 中使用 ECJ 編譯出現(xiàn) java.lang.IllegalArgumentException問題 ,本文內(nèi)容簡(jiǎn)短給大家介紹的好,需要的朋友可以參考下2020-05-05Java中的 FilterInputStream簡(jiǎn)介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
FilterInputStream 的作用是用來(lái)“封裝其它的輸入流,并為它們提供額外的功能”。接下來(lái)通過本文給大家分享Java中的 FilterInputStream簡(jiǎn)介,感興趣的朋友一起學(xué)習(xí)吧2017-05-05詳解Spring Security如何在權(quán)限中使用通配符
小伙伴們知道,在Shiro中,默認(rèn)是支持權(quán)限通配符的。現(xiàn)在給用戶授權(quán)的時(shí)候,可以一個(gè)權(quán)限一個(gè)權(quán)限的配置,也可以直接用通配符。本文將介紹Spring Security如何在權(quán)限中使用通配符,需要的可以參考一下2022-06-06