Springboot+SpringSecurity+JWT實現使用者登入和許可權認證示例
如今,網際網路專案對於安全的要求越來越嚴格,這就是對後端開發提出了更多的要求,目前比較成熟的幾種大家比較熟悉的模式,像RBAC 基於角色許可權的驗證,shiro框架專門用於處理許可權方面的,另一個比較流行的後端框架是Spring-Security,該框架提供了一整套比較成熟,也很完整的機制用於處理各類場景下的可以基於許可權,資源路徑,以及授權方面的解決方案,部分模組支援定製化,而且在和oauth2.0進行了很好的無縫連線,在移動網際網路的授權認證方面有很強的優勢,具體的使用大家可以結合自己的業務場景進行選取和使用
下面來說說關於單點登入中目前比較流行的一種使用方式,就是springsecurity+jwt實現無狀態下使用者登入;
JWT
在之前的篇章中大致提到過,使用jwt在分散式專案中進行使用者資訊的認證很方便,各個模組只需要知道配置的祕鑰,就可以解密token中使用者的基本資訊,完成認證,很方便,關於使用jwt的基本內容可以查閱相關資料,或者參考我之前的一篇;
整理一下思路
1、搭建springboot工程
2、匯入springSecurity跟jwt的依賴
3、使用者的實體類,dao層,service層(真正開發時再寫,這裡就直接呼叫dao層操作資料庫)
4、實現UserDetailsService介面
5、實現UserDetails介面
6、驗證使用者登入資訊的攔截器
7、驗證使用者許可權的攔截器
8、springSecurity配置
專案結構
pom檔案
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.5.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure --> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.4.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mybatis依賴 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- redis依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>
application.properties
server.port=8091 #資料庫連線 spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.username=root spring.datasource.password=root #mybatis配置 mybatis.type-aliases-package=com.congge.entity mybatis.mapper-locations=classpath:mybatis/*.xml #redis配置 spring.session.store-type=redis spring.redis.database=0 spring,redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.pool.min-idle=10000 spring.redis.timeout=30000
為模擬使用者登入,這裡提前建立了一個測試使用的表,user
實體類User
public class User { private Integer id; private String username; private String password; private String role; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } @Override public String toString() { return "User{" + "id=" + id + ",username='" + username + '\'' + ",password='" + password + '\'' + ",role='" + role + '\'' + '}'; } }
Jwt工具類,用於管理token相關的操作,可以單測使用
public class TestJwtUtils { public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; public static final String SUBJECT = "congge"; public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7; public static final String APPSECRET_KEY = "congge_secret"; private static final String ROLE_CLAIMS = "rol"; public static String generateJsonWebToken(Users user) { if (user.getId() == null || user.getUserName() == null || user.getFaceImage() == null) { return null; } Map<String,Object> map = new HashMap<>(); map.put(ROLE_CLAIMS,"rol"); String token = Jwts .builder() .setSubject(SUBJECT) .setClaims(map) .claim("id",user.getId()) .claim("name",user.getUserName()) .claim("img",user.getFaceImage()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256,APPSECRET_KEY).compact(); return token; } /** * 生成token * @param username * @param role * @return */ public static String createToken(String username,String role) { Map<String,role); String token = Jwts .builder() .setSubject(username) .setClaims(map) .claim("username",username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256,APPSECRET_KEY).compact(); return token; } public static Claims checkJWT(String token) { try { final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 獲取使用者名稱 * @param token * @return */ public static String getUsername(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("username").toString(); } /** * 獲取使用者角色 * @param token * @return */ public static String getUserRole(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("rol").toString(); } /** * 是否過期 * @param token * @return */ public static boolean isExpiration(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); } public static void main(String[] args) { String name = "acong"; String role = "rol"; String token = createToken(name,role); System.out.println(token); Claims claims = checkJWT(token); System.out.println(claims.get("username")); System.out.println(getUsername(token)); System.out.println(getUserRole(token)); System.out.println(isExpiration(token)); } /** * eyJhbGciOiJIUzI1NiJ9. * eyJzdWIiOiJjb25nZ2UiLCJpZCI6IjExMDExIiwibmFtZSI6Im51b3dlaXNpa2kiLCJpbWciOiJ3d3cudW9rby5jb20vMS5wbmciLCJpYXQiOjE1NTQ5OTI1NzksImV4cCI6MTU1NTU5NzM3OX0. * 6DJ9En-UBcTiMRldZeevJq3e1NxJgOWryUyim4_-tEE * * @param args */ /*public static void main(String[] args) { Users user = new Users(); user.setId("11011"); user.setUserName("nuoweisiki"); user.setFaceImage("www.uoko.com/1.png"); String token = generateJsonWebToken(user); System.out.println(token); Claims claims = checkJWT(token); if (claims != null) { String id = claims.get("id").toString(); String name = claims.get("name").toString(); String img = claims.get("img").toString(); String rol = claims.get("rol").toString(); System.out.println("id:" + id); System.out.println("name:" + name); System.out.println("img:" + img); System.out.println("rol:" + rol); } }*/ }
操作資料庫的類
,這裡主要是提供使用者註冊的一個save使用者的方法,
@Service public class UserService { @Autowired private UserDao userDao; public void save(User user) { user.setId(1); userDao.save(user); } }
JwtUser
該類封裝登入使用者相關資訊,例如使用者名稱,密碼,許可權集合等,需要實現UserDetails 介面,
public class JwtUser implements UserDetails { private Integer id; private String username; private String password; private Collection<? extends GrantedAuthority> authorities; public JwtUser() { } // 寫一個能直接使用user建立jwtUser的構造器 public JwtUser(User user) { id = user.getId(); username = user.getUsername(); password = user.getPassword(); authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole())); } public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } public String getPassword() { return password; } public String getUsername() { return username; } public boolean isAccountNonExpired() { return true; } public boolean isAccountNonLocked() { return true; } public boolean isCredentialsNonExpired() { return true; } public boolean isEnabled() { return true; } @Override public String toString() { return "JwtUser{" + "id=" + id + ",authorities=" + authorities + '}'; } }
配置攔截器
JWTAuthenticationFilter
JWTAuthenticationFilter繼承於UsernamePasswordAuthenticationFilter
該攔截器用於獲取使用者登入的資訊,只需建立一個token並呼叫authenticationManager.authenticate()讓spring-security去進行驗證就可以了,不用自己查資料庫再對比密碼了,這一步交給spring去操作。 這個操作有點像是shiro的subject.login(new UsernamePasswordToken()),驗證的事情交給框架。
/** * 驗證使用者名稱密碼正確後,生成一個token,並將token返回給客戶端 * 該類繼承自UsernamePasswordAuthenticationFilter,重寫了其中的2個方法,* attemptAuthentication:接收並解析使用者憑證。 * successfulAuthentication:使用者成功登入後,這個方法會被呼叫,我們在這個方法裡生成token並返回。 */ public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; super.setFilterProcessesUrl("/auth/login"); } @Override public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException { // 從輸入流中獲取到登入的資訊 try { LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(),LoginUser.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginUser.getUsername(),loginUser.getPassword()) ); } catch (IOException e) { e.printStackTrace(); return null; } } // 成功驗證後呼叫的方法 // 如果驗證成功,就生成token並返回 @Override protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException,ServletException { JwtUser jwtUser = (JwtUser) authResult.getPrincipal(); System.out.println("jwtUser:" + jwtUser.toString()); String role = ""; Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities(); for (GrantedAuthority authority : authorities){ role = authority.getAuthority(); } String token = TestJwtUtils.createToken(jwtUser.getUsername(),role); //String token = JwtTokenUtils.createToken(jwtUser.getUsername(),false); // 返回建立成功的token // 但是這裡建立的token只是單純的token // 按照jwt的規定,最後請求的時候應該是 `Bearer token` response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); String tokenStr = JwtTokenUtils.TOKEN_PREFIX + token; response.setHeader("token",tokenStr); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request,AuthenticationException failed) throws IOException,ServletException { response.getWriter().write("authentication failed,reason: " + failed.getMessage()); } }
JWTAuthorizationFilter
驗證成功當然就是進行鑑權了,每一次需要許可權的請求都需要檢查該使用者是否有該許可權去操作該資源,當然這也是框架幫我們做的,那麼我們需要做什麼呢?很簡單,只要告訴spring-security該使用者是否已登入,是什麼角色,擁有什麼許可權就可以了。
JWTAuthenticationFilter繼承於BasicAuthenticationFilter,至於為什麼要繼承這個我也不太清楚了,這個我也是網上看到的其中一種實現,實在springSecurity苦手,不過我覺得不繼承這個也沒事呢(實現以下filter介面或者繼承其他filter實現子類也可以吧)只要確保過濾器的順序,JWTAuthorizationFilter在JWTAuthenticationFilter後面就沒問題了。
/** * 驗證成功當然就是進行鑑權了 * 登入成功之後走此類進行鑑權操作 */ public class JWTAuthorizationFilter extends BasicAuthenticationFilter { public JWTAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request,FilterChain chain) throws IOException,ServletException { String tokenHeader = request.getHeader(TestJwtUtils.TOKEN_HEADER); // 如果請求頭中沒有Authorization資訊則直接放行了 if (tokenHeader == null || !tokenHeader.startsWith(TestJwtUtils.TOKEN_PREFIX)) { chain.doFilter(request,response); return; } // 如果請求頭中有token,則進行解析,並且設定認證資訊 SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader)); super.doFilterInternal(request,response,chain); } // 這裡從token中獲取使用者資訊並新建一個token private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) { String token = tokenHeader.replace(TestJwtUtils.TOKEN_PREFIX,""); String username = TestJwtUtils.getUsername(token); String role = TestJwtUtils.getUserRole(token); if (username != null){ return new UsernamePasswordAuthenticationToken(username,null,Collections.singleton(new SimpleGrantedAuthority(role)) ); } return null; } }
配置SpringSecurity
到這裡基本操作都寫好啦,現在就需要我們將這些辛苦寫好的“元件”組合到一起發揮作用了,那就需要配置了。需要開啟一下註解@EnableWebSecurity然後再繼承一下WebSecurityConfigurerAdapter就可以啦,
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .authorizeRequests() // 測試用資源,需要驗證了的使用者才能訪問 .antMatchers("/tasks/**") .authenticated() .antMatchers(HttpMethod.DELETE,"/tasks/**") .hasRole("ADMIN") // 其他都放行了 .anyRequest().permitAll() .and() .addFilter(new JWTAuthenticationFilter(authenticationManager())) .addFilter(new JWTAuthorizationFilter(authenticationManager())) // 不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .exceptionHandling() .authenticationEntryPoint(new JWTAuthenticationEntryPoint()); } @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues()); return source; } }
AuthController
測試類,模擬使用者註冊,
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private UserService userService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @PostMapping("/register") public String registerUser(@RequestBody Map<String,String> registerUser){ User user = new User(); user.setUsername(registerUser.get("username")); user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password"))); user.setRole("ROLE_USER"); userService.save(user); return "success"; } }
註冊是有了,那登入在哪呢?我們看一下UsernamePasswordAuthenticationFilter的原始碼
public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login","POST")); }
可以看出來預設是/login,所以登入直接使用這個路徑就可以啦~當然也可以自定義
只需要在JWTAuthenticationFilter的構造方法中加入下面那一句話就可以啦
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; super.setFilterProcessesUrl("/auth/login"); }
所以現在認證的路徑統一了一下也是挺好的~看起來相當舒服了
註冊:/auth/register
登入:/auth/login
TaskController
提供一個外部訪問的API資源介面,即使用者要訪問該類下面的介面必須要先通過認證,後面的測試中也可以看出來,直接貼程式碼,
@RequestMapping("/tasks") public class TaskController { @GetMapping("/getTasks") @ResponseBody public String listTasks(){ return "任務列表"; } @PostMapping @PreAuthorize("hasRole('ADMIN')") public String newTasks(){ return "建立了一個新的任務"; } }
下面我們來測試一下,為了模擬效果比較直觀點,我們使用postMan進行測試,
1、首先,我們呼叫註冊的方法註冊一個使用者,
註冊成功之後,我們看到資料庫已經有了一個使用者,
2、使用該使用者進行登入,我們希望的是登入成功之後,後臺生成一個token並返回給前端,這樣後面的介面呼叫中直接帶上這個token即可,
可以看到登入成功,後臺反返回了token,下面我們使用這個token請求其他的介面,測試一下getTasks這個介面,注意需要在postMan的請求header裡面帶上token資訊,這裡是全部的token,即包含Bearer 的整個字串,
這時候,成功請求到了介面的資料,大家可以測試一下將過期時間調整的短一點,然後再去請求看看會有什麼樣的效果,這裡就不做演示了。
本篇到這裡基本就結束了,關於springsecurity其實內容還是很多的,裡面的用法也比較複雜,大家抽空可以做深入的研究,篇幅原因不做過多介紹了。最後感謝觀看。
附上原始碼地址:boot-ssoserver_jb51.rar
到此這篇關於Springboot+SpringSecurity+JWT實現使用者登入和許可權認證示例的文章就介紹到這了,更多相關Springboot SpringSecurity JWT內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!