SpringSecurity+JWT實(shí)現(xiàn)前后端分離的使用詳解
創(chuàng)建一個(gè)配置類 SecurityConfig 繼承 WebSecurityConfigurerAdapter
package top.ryzeyang.demo.common.config; import org.springframework.context.annotation.Bean; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; 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.builders.WebSecurity; 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.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import top.ryzeyang.demo.common.filter.JwtAuthenticationTokenFilter; import top.ryzeyang.demo.utils.JwtTokenUtil; @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { final AuthenticationFailureHandler authenticationFailureHandler; final AuthenticationSuccessHandler authenticationSuccessHandler; final AuthenticationEntryPoint authenticationEntryPoint; final AccessDeniedHandler accessDeniedHandler; final LogoutSuccessHandler logoutSuccessHandler; public SecurityConfig(AuthenticationFailureHandler authenticationFailureHandler, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationEntryPoint authenticationEntryPoint, AccessDeniedHandler accessDeniedHandler, LogoutSuccessHandler logoutSuccessHandler) { this.authenticationFailureHandler = authenticationFailureHandler; this.authenticationSuccessHandler = authenticationSuccessHandler; this.authenticationEntryPoint = authenticationEntryPoint; this.accessDeniedHandler = accessDeniedHandler; this.logoutSuccessHandler = logoutSuccessHandler; } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean("users") public UserDetailsService users() { UserDetails user = User.builder() .username("user") .password("{bcrypt}$2a$10$1.NSMxlOyMgJHxOi8CWwxuU83G0/HItXxRoBO4QWZMTDp0tzPbCf.") .roles("USER") // roles 和 authorities 不能并存 // .authorities(new String[]{"system:user:query", "system:user:edit", "system:user:delete"}) .build(); UserDetails admin = User.builder() .username("admin") .password("{bcrypt}$2a$10$1.NSMxlOyMgJHxOi8CWwxuU83G0/HItXxRoBO4QWZMTDp0tzPbCf.") // .roles("USER", "ADMIN") .roles("ADMIN") // .authorities("system:user:create") .build(); return new InMemoryUserDetailsManager(user, admin); } /** * 角色繼承: * 讓Admin角色擁有User的角色的權(quán)限 * @return */ @Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl result = new RoleHierarchyImpl(); result.setHierarchy("ROLE_ADMIN > ROLE_USER"); return result; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(users()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); } @Override protected void configure(HttpSecurity http) throws Exception { // 自定義異常處理 http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) // 權(quán)限 .and() .authorizeRequests() // 一個(gè)是必須待身份信息但是不校驗(yàn)權(quán)限。 .antMatchers("/", "/mock/login").permitAll() //只允許匿名訪問(wèn) .antMatchers("/hello").anonymous() .anyRequest() .authenticated() // 表單登錄 // .and() // .formLogin() // .successHandler(authenticationSuccessHandler) // .failureHandler(authenticationFailureHandler) // .loginProcessingUrl("/login") // .permitAll() // 注銷 .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(logoutSuccessHandler) .permitAll() // 關(guān)閉csrf 會(huì)在頁(yè)面中生成一個(gè)csrf_token .and() .csrf() .disable() // 基于token,所以不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 添加我們的JWT過(guò)濾器 .and() .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class) ; } @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public JwtTokenUtil jwtTokenUtil() { return new JwtTokenUtil(); } }
創(chuàng)建JWT過(guò)濾器 繼承 OncePerRequestFilter
這里直接用的macro大佬 mall商城里的例子
package top.ryzeyang.demo.common.filter; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; 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.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import top.ryzeyang.demo.utils.JwtTokenUtil; 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)過(guò)濾器 * * @author macro * @date 2018/4/26 */ @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Qualifier("users") @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); log.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)); log.info("authenticated user:{}", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
自定義handler
這里主要介紹兩個(gè)hanler,一個(gè)權(quán)限不足,一個(gè)驗(yàn)證失敗的
權(quán)限不足 實(shí)現(xiàn) AccessDeniedHandler
package top.ryzeyang.demo.common.handler; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import top.ryzeyang.demo.common.api.CommonResult; import top.ryzeyang.demo.common.api.ResultEnum; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component public class MyAccessDeineHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); writer.write(new ObjectMapper().writeValueAsString(new CommonResult<>(ResultEnum.ACCESS_ERROR, e.getMessage()))); writer.flush(); writer.close(); } }
認(rèn)證失敗 實(shí)現(xiàn) AuthenticationEntryPoint
如賬號(hào)密碼錯(cuò)誤等驗(yàn)證不通過(guò)時(shí)
package top.ryzeyang.demo.common.handler; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import top.ryzeyang.demo.common.api.CommonResult; import top.ryzeyang.demo.common.api.ResultEnum; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); // 認(rèn)證失敗 writer.write(new ObjectMapper().writeValueAsString(new CommonResult<>(ResultEnum.AUTHENTICATION_ERROR, e.getMessage()))); writer.flush(); writer.close(); } }
JWT工具類
這里直接用的macro大佬 mall商城里的例子
稍微改了一點(diǎn),因?yàn)?JDK11 用的 jjwt 版本不一樣 ,語(yǔ)法也有些不同
pom 文件中引入 jjwt
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.2</version> <scope>runtime</scope> </dependency>
JwtTokenUtil
package top.ryzeyang.demo.utils; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JwtToken生成的工具類 * JWT token的格式:header.payload.signature * header的格式(算法、token的類型): * {"alg": "HS512","typ": "JWT"} * payload的格式(用戶名、創(chuàng)建時(shí)間、生成時(shí)間): * {"sub":"wang","created":1489079981393,"exp":1489684781} * signature的生成算法: * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) * * @author macro * @date 2018/4/26 */ @Slf4j public class JwtTokenUtil { 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; private SecretKey getSecretKey() { byte[] encodeKey = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(encodeKey); } /** * 根據(jù)負(fù)責(zé)生成JWT的token */ private String generateToken(Map<String, Object> claims) { SecretKey secretKey = getSecretKey(); return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(secretKey) .compact(); } /** * 測(cè)試生成的token * @param claims * @return */ public String generateToken2(Map<String, Object> claims) { SecretKey secretKey = getSecretKey(); return Jwts.builder() .setClaims(claims) .setIssuer("Java4ye") .setExpiration(new Date(System.currentTimeMillis() + 1 * 1000)) .signWith(secretKey) .compact(); } /** * 從token中獲取JWT中的負(fù)載 */ private Claims getClaimsFromToken(String token) { SecretKey secretKey = getSecretKey(); Claims claims = null; try { claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); } catch (Exception e) { log.info("JWT格式驗(yàn)證失敗:{}", token); } return claims; } /** * 生成token的過(guò)期時(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中獲取過(guò)期時(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); } /** * 當(dāng)原來(lái)的token沒(méi)過(guò)期時(shí)是可以刷新的 * * @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校驗(yàn)不通過(guò) Claims claims = getClaimsFromToken(token); if (claims == null) { return null; } //如果token已經(jīng)過(guò)期,不支持刷新 if (isTokenExpired(token)) { return null; } //如果token在30分鐘之內(nèi)剛刷新過(guò),返回原token if (tokenRefreshJustBefore(token, 30 * 60)) { return token; } else { claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } } /** * 判斷token在指定時(shí)間內(nèi)是否剛剛刷新過(guò) * * @param token 原token * @param time 指定時(shí)間(秒) */ private boolean tokenRefreshJustBefore(String token, int time) { Claims claims = getClaimsFromToken(token); Date created = claims.get(CLAIM_KEY_CREATED, Date.class); Date refreshDate = new Date(); //刷新時(shí)間在創(chuàng)建時(shí)間的指定時(shí)間內(nèi) if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) { return true; } return false; } }
配置文件 application.yml
這里的 secret 可以用該方法生成
@Test void generateKey() { /** * SECRET 是簽名密鑰,只生成一次即可,生成方法: * Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); * String secretString = Encoders.BASE64.encode(key.getEncoded()); # 本文使用 BASE64 編碼 * */ Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512); String secretString = Encoders.BASE64.encode(key.getEncoded()); System.out.println(secretString); // Blk1X8JlN4XH4s+Kuc0YUFXv+feyTgVUMycSiKbiL0YhRddy872mCNZBGZIb57Jn2V1RtaFXIxs8TvNPsnG//g== }
jwt: tokenHeader: Authorization secret: Blk1X8JlN4XH4s+Kuc0YUFXv+feyTgVUMycSiKbiL0YhRddy872mCNZBGZIb57Jn2V1RtaFXIxs8TvNPsnG//g== expiration: 604800 tokenHead: 'Bearer ' server: servlet: context-path: /api
Controller
AuthController
package top.ryzeyang.demo.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.bind.annotation.*; import top.ryzeyang.demo.common.api.CommonResult; import top.ryzeyang.demo.model.dto.UserDTO; import top.ryzeyang.demo.utils.CommonResultUtil; import top.ryzeyang.demo.utils.JwtTokenUtil; import java.util.Collection; @RestController @ResponseBody @RequestMapping("/mock") public class AuthController { @Autowired private JwtTokenUtil jwtTokenUtil; @Qualifier("users") @Autowired private UserDetailsService userDetailsService; @Autowired AuthenticationManager authenticationManager; @GetMapping("/userinfo") public CommonResult<Collection<? extends GrantedAuthority>> getUserInfo(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); return CommonResultUtil.success(authorities); } /** * 模擬登陸 */ @PostMapping("/login") public CommonResult<String> login(@RequestBody UserDTO userDTO){ String username = userDTO.getUsername(); String password = userDTO.getPassword(); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); Authentication authenticate = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authenticate); UserDetails userDetails = userDetailsService.loadUserByUsername(username); String t = jwtTokenUtil.generateToken(userDetails); return CommonResultUtil.success(t); } }
HelloController
package top.ryzeyang.demo.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } @GetMapping("/hello/anonymous") public String hello2() { return "anonymous"; } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/hello/admin") public String helloAdmin() { return "hello Admin"; } @PreAuthorize("hasRole('USER')") @GetMapping("/hello/user") public String helloUser() { return "hello user"; } @PreAuthorize("hasAnyAuthority('system:user:query')") @GetMapping("/hello/user2") public String helloUser2() { return "hello user2"; } }
項(xiàng)目地址在GitHub上
地址:SpringSecurity-Vuetify-Permissions-demo
到此這篇關(guān)于SpringSecurity+JWT實(shí)現(xiàn)前后端分離的使用詳解的文章就介紹到這了,更多相關(guān)SpringSecurity+JWT前后端分離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot詳解如何實(shí)現(xiàn)SQL注入過(guò)濾器過(guò)程
這篇文章主要介紹了基于springboot實(shí)現(xiàn)SQL注入過(guò)濾器,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2022-06-06Java遠(yuǎn)程調(diào)用Shell腳本并獲取輸出信息【推薦】
這篇文章主要介紹了Java遠(yuǎn)程調(diào)用Shell腳本并獲取輸出信息,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09JAVA浮點(diǎn)數(shù)計(jì)算精度損失底層原理與解決方案
本文主要介紹了JAVA浮點(diǎn)數(shù)計(jì)算精度損失底層原理與解決方案。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02MyBatis-plus數(shù)據(jù)庫(kù)字段排序不準(zhǔn)確的解決
這篇文章主要介紹了MyBatis-plus數(shù)據(jù)庫(kù)字段排序不準(zhǔn)確的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Java 1.8使用數(shù)組實(shí)現(xiàn)循環(huán)隊(duì)列
這篇文章主要為大家詳細(xì)介紹了Java 1.8使用數(shù)組實(shí)現(xiàn)循環(huán)隊(duì)列,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10Spring?Boot自定義Starter組件開(kāi)發(fā)實(shí)現(xiàn)配置過(guò)程
SpringBoot中的starter是一種非常重要的機(jī)制,能夠拋棄以前繁雜的配置,將其統(tǒng)一集成進(jìn)?starter,應(yīng)用者只需要在maven中引入starter依賴,這篇文章主要介紹了Spring?Boot自定義Starter組件開(kāi)發(fā)實(shí)現(xiàn),需要的朋友可以參考下2022-06-06Java后端向前端返回文件流實(shí)現(xiàn)下載功能的方法
這篇文章主要給大家介紹了關(guān)于Java后端向前端返回文件流實(shí)現(xiàn)下載功能的相關(guān)資料,Java后端可以通過(guò)調(diào)用接口返回文件流來(lái)實(shí)現(xiàn)文件傳輸功能,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-10-10Java中的Random和ThreadLocalRandom詳細(xì)解析
這篇文章主要介紹了Java中的Random和ThreadLocalRandom詳細(xì)解析,Random 類用于生成偽隨機(jī)數(shù)的流, 該類使用48位種子,其使用線性同余公式進(jìn)行修改,需要的朋友可以參考下2024-01-01