SpringBoot集成SpringSecurity和JWT做登陸鑒權的實現(xiàn)
廢話
目前流行的前后端分離讓Java程序員可以更加專注的做好后臺業(yè)務邏輯的功能實現(xiàn),提供如返回Json格式的數(shù)據(jù)接口就可以。SpringBoot的易用性和對其他框架的高度集成,用來快速開發(fā)一個小型應用是最佳的選擇。
一套前后端分離的后臺項目,剛開始就要面對的就是登陸和授權的問題。這里提供一套方案供大家參考。
主要看點:
- 登陸后獲取token,根據(jù)token來請求資源
- 根據(jù)用戶角色來確定對資源的訪問權限
- 統(tǒng)一異常處理
- 返回標準的Json格式數(shù)據(jù)
正文
首先是pom文件:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--這是不是必須,只是我引用了里面一些類的方法--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-solr</artifactId> </dependency> <!--這是不是必須,只是我引用了里面一些類的方法--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
application.yml:
spring : datasource : url : jdbc:mysql://127.0.0.1:3306/les_data_center?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false username : root password : 123456 driverClassName : com.mysql.jdbc.Driver jackson: data-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis : config-location : classpath:/mybatis-config.xml # JWT jwt: header: Authorization secret: mySecret #token有效期一天 expiration: 86400 tokenHead: "Bearer "
接著是對security的配置,讓security來保護我們的API
SpringBoot推薦使用配置類來代替xml配置。那這里,我也使用配置類的方式。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final JwtAuthenticationEntryPoint unauthorizedHandler; private final AccessDeniedHandler accessDeniedHandler; private final UserDetailsService CustomUserDetailsService; private final JwtAuthenticationTokenFilter authenticationTokenFilter; @Autowired public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, @Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler, @Qualifier("CustomUserDetailsService") UserDetailsService CustomUserDetailsService, JwtAuthenticationTokenFilter authenticationTokenFilter) { this.unauthorizedHandler = unauthorizedHandler; this.accessDeniedHandler = accessDeniedHandler; this.CustomUserDetailsService = CustomUserDetailsService; this.authenticationTokenFilter = authenticationTokenFilter; } @Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder // 設置UserDetailsService .userDetailsService(this.CustomUserDetailsService) // 使用BCrypt進行密碼的hash .passwordEncoder(passwordEncoder()); } // 裝載BCrypt密碼編碼器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and() // 由于使用的是JWT,我們這里不需要csrf .csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() // 對于獲取token的rest api要允許匿名訪問 .antMatchers("/api/v1/auth", "/api/v1/signout", "/error/**", "/api/**").permitAll() // 除上面外的所有請求全部需要鑒權認證 .anyRequest().authenticated(); // 禁用緩存 httpSecurity.headers().cacheControl(); // 添加JWT filter httpSecurity .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui", "/swagger-resources", "/swagger-resources/configuration/security", "/swagger-ui.html" ); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
該類中配置了幾個bean來供security使用。
- JwtAuthenticationTokenFilter:token過濾器來驗證token有效性
- UserDetailsService:實現(xiàn)了DetailsService接口,用來做登陸驗證
- JwtAuthenticationEntryPoint :認證失敗處理類
- RestAuthenticationAccessDeniedHandler: 權限不足處理類
那么,接下來一個一個實現(xiàn)這些類:
/** * token校驗,引用的stackoverflow一個答案里的處理方式 * Author: JoeTao * createAt: 2018/9/14 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Value("${jwt.header}") private String token_header; @Resource private JWTUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String auth_token = request.getHeader(this.token_header); final String auth_token_start = "Bearer "; if (StringUtils.isNotEmpty(auth_token) && auth_token.startsWith(auth_token_start)) { auth_token = auth_token.substring(auth_token_start.length()); } else { // 不按規(guī)范,不允許通過驗證 auth_token = null; } String username = jwtUtils.getUsernameFromToken(auth_token); logger.info(String.format("Checking authentication for user %s.", username)); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { User user = jwtUtils.getUserFromToken(auth_token); if (jwtUtils.validateToken(auth_token, user)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); logger.info(String.format("Authenticated user %s, setting security context", username)); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } }
/** * 認證失敗處理類,返回401 * Author: JoeTao * createAt: 2018/9/20 */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { //驗證為未登陸狀態(tài)會進入此方法,認證錯誤 System.out.println("認證失?。? + authException.getMessage()); response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter printWriter = response.getWriter(); String body = ResultJson.failure(ResultCode.UNAUTHORIZED, authException.getMessage()).toString(); printWriter.write(body); printWriter.flush(); } }
因為我們使用的REST API,所以我們認為到達后臺的請求都是正常的,所以返回的HTTP狀態(tài)碼都是200,用接口返回的code來確定請求是否正常。
/** * 權限不足處理類,返回403 * Author: JoeTao * createAt: 2018/9/21 */ @Component("RestAuthenticationAccessDeniedHandler") public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { //登陸狀態(tài)下,權限不足執(zhí)行該方法 System.out.println("權限不足:" + e.getMessage()); response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter printWriter = response.getWriter(); String body = ResultJson.failure(ResultCode.FORBIDDEN, e.getMessage()).toString(); printWriter.write(body); printWriter.flush(); } }
/** * 登陸身份認證 * Author: JoeTao * createAt: 2018/9/14 */ @Component(value="CustomUserDetailsService") public class CustomUserDetailsService implements UserDetailsService { private final AuthMapper authMapper; public CustomUserDetailsService(AuthMapper authMapper) { this.authMapper = authMapper; } @Override public User loadUserByUsername(String name) throws UsernameNotFoundException { User user = authMapper.findByUsername(name); if (user == null) { throw new UsernameNotFoundException(String.format("No user found with username '%s'.", name)); } Role role = authMapper.findRoleByUserId(user.getId()); user.setRole(role); return user; } }
登陸邏輯:
public ResponseUserToken login(String username, String password) { //用戶驗證 final Authentication authentication = authenticate(username, password); //存儲認證信息 SecurityContextHolder.getContext().setAuthentication(authentication); //生成token final User user = (User) authentication.getPrincipal(); // User user = (User) userDetailsService.loadUserByUsername(username); final String token = jwtTokenUtil.generateAccessToken(user); //存儲token jwtTokenUtil.putToken(username, token); return new ResponseUserToken(token, user); } private Authentication authenticate(String username, String password) { try { //該方法會去調用userDetailsService.loadUserByUsername()去驗證用戶名和密碼,如果正確,則存儲該用戶名密碼到“security 的 context中” return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (DisabledException | BadCredentialsException e) { throw new CustomException(ResultJson.failure(ResultCode.LOGIN_ERROR, e.getMessage())); } }
自定義異常:
@Getter public class CustomException extends RuntimeException{ private ResultJson resultJson; public CustomException(ResultJson resultJson) { this.resultJson = resultJson; } }
統(tǒng)一異常處理:
/** * 異常處理類 * controller層異常無法捕獲處理,需要自己處理 * Created by jt on 2018/8/27. */ @RestControllerAdvice @Slf4j public class DefaultExceptionHandler { /** * 處理所有自定義異常 * @param e * @return */ @ExceptionHandler(CustomException.class) public ResultJson handleCustomException(CustomException e){ log.error(e.getResultJson().getMsg().toString()); return e.getResultJson(); } }
所有經controller轉發(fā)的請求拋出的自定義異常都會被捕獲處理,一般情況下就是返回給調用方一個json的報錯信息,包含自定義狀態(tài)碼、錯誤信息及補充描述信息。
值得注意的是,在請求到達controller之前,會被Filter攔截,如果在controller或者之前拋出的異常,自定義的異常處理器是無法處理的,需要自己重新定義一個全局異常處理器或者直接處理。
Filter攔截請求兩次的問題
跨域的post的請求會驗證兩次,get不會。網上的解釋是,post請求第一次是預檢請求,Request Method: OPTIONS。
解決方法:
在webSecurityConfig里添加
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
就可以不攔截options請求了。
這里只給出了最主要的代碼,還有controller層的訪問權限設置,返回狀態(tài)碼,返回類定義等等。
所有代碼已上傳GitHub,項目地址
到此這篇關于SpringBoot集成SpringSecurity和JWT做登陸鑒權的實現(xiàn)的文章就介紹到這了,更多相關SpringBoot JWT 登陸鑒權內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- Springboot整合SpringSecurity實現(xiàn)登錄認證和鑒權全過程
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權認證
- SpringBoot整合SpringSecurityOauth2實現(xiàn)鑒權動態(tài)權限問題
- SpringSecurity動態(tài)加載用戶角色權限實現(xiàn)登錄及鑒權功能
- springboot+jwt+springSecurity微信小程序授權登錄問題
- SpringSecurity實現(xiàn)權限認證與授權的使用示例
- SpringSecurity進行認證與授權的示例代碼
- springSecurity用戶認證和授權的實現(xiàn)
- 深入淺析springsecurity入門登錄授權
- mall整合SpringSecurity及JWT實現(xiàn)認證授權實戰(zhàn)
- SpringSecurity頁面授權與登錄驗證實現(xiàn)(內存取值與數(shù)據(jù)庫取值)
- SpringSecurity 鑒權與授權的具體使用
相關文章
spring boot實戰(zhàn)之內嵌容器tomcat配置
本篇文章主要介紹了Spring Boot 使用內嵌的tomcat容器配置,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01IDEA 非常重要的一些設置項(一連串的問題差點讓我重新用回 Eclipse)
這篇文章主要介紹了IDEA 非常重要的一些設置項(一連串的問題差點讓我重新用回 Eclipse),本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08mybatisplus?復合主鍵(多主鍵)?CRUD示例詳解
這篇文章主要介紹了mybatisplus?復合主鍵(多主鍵)?CRUD實例詳解,本文通過實例代碼圖文相結合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03Java實現(xiàn)前端jsencrypt.js加密后端解密的示例代碼
這篇文章主要為大家詳細介紹了如何利用jsencrypt.js實現(xiàn)前端加密,利用Java實現(xiàn)后端解密的功能,文中的示例代碼講解詳細,需要的可以參考一下2022-09-09