一,為什么使用jwt?
1,什么是jwt?
Json Web Token,
它是JSON風格的輕量級的授權和身份認證規范,
可以實現無狀態、分布式的Web應用授權
2,jwt的官網:
https://jwt.io/
java實現的jwt的開源項目:
https://github.com/jwtk/jjwt
3,使用jwt的好處?
客戶端請求不依賴服務端的信息,多次向服務端請求不需要必須訪問到同一台物理服務器上
服務端的集群和狀態對客戶端透明
服務端可以任意的遷移和伸縮,方便進行集群化部署
減小服務端存儲壓力
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息
1,項目地址:
https://github.com/liuhongdi/securityjwt
2,項目功能說明:
演示了使用jwt保存用戶token,
適用於接口站的用戶信息保存
3,項目結構;如圖:
三,配置文件說明
1,pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--security begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--jjwt begin--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--thymeleaf begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--fastjson begin--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.73</version> </dependency> <!--jaxb--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!--mysql mybatis begin--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
2,application.properties
#error server.error.include-stacktrace=always #error logging.level.org.springframework.web=trace #thymeleaf spring.thymeleaf.cache=false spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.mode=HTML spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html #mysql spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false spring.datasource.username=root spring.datasource.password=lhddemo spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis mybatis.mapper-locations=classpath:/mapper/*Mapper.xml mybatis.type-aliases-package=com.example.demo.mapper
3,數據表:
建表sql:
CREATE TABLE `sys_user` ( `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用戶名', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密碼', `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵稱', PRIMARY KEY (`userId`), UNIQUE KEY `userName` (`userName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶表'
INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES (1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老劉'), (2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理員'), (3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商戶老張');
說明:3個密碼都是111111,僅供演示使用
CREATE TABLE `sys_user_role` ( `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用戶id', `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id', PRIMARY KEY (`urId`), UNIQUE KEY `userId` (`userId`,`roleName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶角色關聯表'
INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES (1, 2, 'ADMIN'), (2, 3, 'MERCHANT');
四, java代碼說明
1,WebSecurityConfig.java
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserAuthenticationEntryPoint userAuthenticationEntryPoint; @Autowired private UserDetailsService jwtUserDetailsService; @Autowired private JwtRequestFilter jwtRequestFilter; @Resource private UserAccessDeniedHandler userAccessDeniedHandler; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 本示例不需要使用CSRF httpSecurity.csrf().disable(); httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); httpSecurity.authorizeRequests().antMatchers("/home/**").permitAll(); // 認證頁面不需要權限 httpSecurity.authorizeRequests(). antMatchers("/auth/authenticate").permitAll(). antMatchers("/admin/**").hasAnyRole("ADMIN"). //其他頁面 anyRequest().authenticated(); //登錄頁面 模擬客戶端 httpSecurity.formLogin().loginPage("/home/login").permitAll(); //access deny httpSecurity.exceptionHandling().accessDeniedHandler(userAccessDeniedHandler); //unauthorized httpSecurity.exceptionHandling().authenticationEntryPoint(userAuthenticationEntryPoint); //驗證請求是否正確 httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
2,UserAuthenticationEntryPoint.java
@Component public class UserAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 當用戶嘗試訪問安全的REST資源而不提供任何憑據時,將調用此方法發送401 響應 System.out.println("i am 401"); ServletUtil.printRestResult(RestResult.error(ResponseCode.WEB_401)); } }
說明:匿名用戶訪問無權限資源時的異常
3,UserAccessDeniedHandler.java
@Component("UserAccessDeniedHandler") public class UserAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { //當用戶在沒有授權的情況下訪問受保護的REST資源時,將調用此方法發送403 Forbidden響應 System.out.println("UserAccessDeniedHandler"); ServletUtil.printRestResult(RestResult.error(ResponseCode.WEB_403)); } }
說明:非匿名用戶訪問無權限訪問的資源時的異常
4,SecUser.java
public class SecUser extends User { //用戶id private int userid; //昵稱 private String nickname; public SecUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } public SecUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public int getUserid() { return userid; } public void setUserid(int userid) { this.userid = userid; } }
擴展spring security user類
5,JwtAuthticationFilter.java
@Component public class JwtAuthticationFilter implements Filter { @Resource private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private JwtUserDetailsService userDetailsService; @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("----------------AuthticationFilter init"); } //過濾功能 @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //得到當前的url HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String path = request.getServletPath(); if (path.equals("/auth/authenticate")) { System.out.println("auth path:"+path); //得到請求的post參數 String username = ""; String password = ""; try { BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream())); StringBuffer sb=new StringBuffer(); String s=null; while((s=br.readLine())!=null){ sb.append(s); } JSONObject jsonObject = JSONObject.parseObject(sb.toString()); username = jsonObject.getString("username"); password = jsonObject.getString("password"); //System.out.println("name:"+name+" age:"+age); } catch (IOException e) { e.printStackTrace(); } System.out.println("username:"+username); System.out.println("password:"+password); String authResult = ""; try{ authResult = authenticate(username,password); } catch (Exception e) { e.printStackTrace(); } System.out.println("authResult:"+authResult);
//驗證通過后生成token返回 if ("success".equals(authResult)) { final UserDetails userDetails = userDetailsService.loadUserByUsername(username); final String token = jwtTokenUtil.generateToken(userDetails); Map<String, String> mapData = new HashMap<String, String>(); mapData.put("token", token); ServletUtil.printRestResult(RestResult.success(mapData)); } else if ("badcredential".equals(authResult)){ ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_FAIL)); } else { ServletUtil.printRestResult(RestResult.error(ResponseCode.ERROR)); } return; } else { System.out.println("not auth path:"+path); filterChain.doFilter(servletRequest, servletResponse); } } @Override public void destroy() { System.out.println("----------------filter destroy"); } private String authenticate(String username, String password) throws Exception { try { System.out.println("username:"+username); System.out.println("password:"+password); authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); System.out.println("authenticate:will return success"); return "success"; } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { System.out.println("BadCredentialsException"); System.out.println(e.toString()); //throw new Exception("INVALID_CREDENTIALS", e); return "badcredential"; } } }
用來實現登錄的filter,驗證通過后生成token返回
6,JwtRequestFilter.java
@Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUserDetailsService jwtUserDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String username = null; String jwtToken = null; // JWT Token 獲取請求頭部的 Bearer System.out.println("filter:header:"+requestTokenHeader); //判斷,從token中得到username if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { //System.out.println("filter :requestTokenHeader not null and start with bearer"); jwtToken = requestTokenHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { System.out.println("JWT Token has expired"); } catch (MalformedJwtException e) { System.out.println("JWT Token MalformedJwtException"); } } else { //System.out.println("filter :requestTokenHeader is null || not start with bearer"); //logger.warn("JWT Token does not begin with Bearer String"); } // 驗證,username,如果驗證合法則保存到SecurityContextHolder if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { //System.out.println("filter:username!=null"); UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username); // JWT 驗證通過 使用Spring Security 管理 if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //System.out.println("usernamePasswordAuthenticationToken:"+usernamePasswordAuthenticationToken.toString()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } else { // System.out.println("jwtTokenUtil.validateToken not success"); } } chain.doFilter(request, response); } }
處理每次的請求,如果有token,則從token獲取用戶信息,驗證用戶信息合法,則把從數據庫中得到的用戶的相關信息保存到SecurityContextHolder
7,JwtUserDetailsService.java
@Service public class JwtUserDetailsService implements UserDetailsService { @Resource private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("-----loadUserByUsername"); SysUser oneUser = sysUserService.getOneUserByUsername(username);//數據庫查詢 看用戶是否存在 String encodedPassword = oneUser.getPassword(); Collection<GrantedAuthority> collection = new ArrayList<>();//權限集合 //用戶角色role前面要添加ROLE_ List<String> roles = oneUser.getRoles(); System.out.println(roles); for (String roleone : roles) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone); collection.add(grantedAuthority); } //給用戶增加用戶id和昵稱 SecUser user = new SecUser(username,encodedPassword,collection); user.setUserid(oneUser.getUserId()); user.setNickname(oneUser.getNickName()); return user; } }
從數據庫得到用戶信息
8,JwtTokenUtil.java
@Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -2550185165626007488L; public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; private String secret = "liuhongdi"; //retrieve username from jwt token public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } //retrieve expiration date from jwt token public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } //for retrieveing any information from token we will need the secret key private Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } //check if the token has expired private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } //generate token for user public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } //generate token private String doGenerateToken(Map<String, Object> claims, String subject) { return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) .signWith(SignatureAlgorithm.HS512, secret).compact(); } //validate token public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
處理JwtToken的工具類,用來生成token,驗證token是否合法
9,login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用 jwt 登錄頁面</title> </head> <body> <div> <input type="text" id="userName" name="userName" value="" placeholder="username"> </div> <div> <input type="password" id="password" name="password" value="" placeholder="password"> </div> <div> <input type="button" id="btnSave" onclick="go_login()" value="登錄"> </div> <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script> <script> //登錄 function go_login() { var username=$("#userName").val(); var password=$("#password").val(); if ($("#userName").val() == "") { alert('userName is empty'); $("#userName").focus(); return false; } if ($("#password").val() == "") { alert('password is empty'); $("#password").focus(); return false; } var postData = { "username":username , "password" : password } $.ajax({ cache: true, type: "POST", url: "/auth/authenticate", contentType: "application/json;charset=UTF-8", data:JSON.stringify(postData), dataType: "json", async: false, error: function (request) { console.log("Connection error"); }, success: function (data) { //save token console.log("data:"); console.log(data); if (data.code == 0) { //success alert("success:"+data.msg+";token:"+data.data.token); //save token localStorage.setItem("token",data.data.token); } else { //failed alert("failed:"+data.msg); } } }); }; </script> </body> </html>
10,其他代碼可從github上查看
五,測試效果
1,登錄,訪問:
http://127.0.0.1:8080/home/login
用admin登錄:
可以看到返回的token
2,查看session信息:訪問:
http://127.0.0.1:8080/home/getsession
點擊:get session info
點擊:get admin info:
可以正常訪問
3,用merchant登錄:
點擊 get admin info:
提示拒絕訪問
六,查看spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.3.RELEASE)