spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)
一,为什么使用jwt?
1,什么是jwt?
Json Web Token,
它是JSON风格的轻量级的授权和身份认证规范,
可以实现无状态、分布式的Web应用授权
2,jwt的官网:
https://jwt.io/
java实现的jwt的开源项目:
https://github.com/jwtk/jjwt
3,使用jwt的好处?
客户端请求不依赖服务端的信息,多次向服务端请求不需要必须访问到同一台物理服务器上
服务端的集群和状态对客户端透明
服务端可以任意的迁移和伸缩,方便进行集群化部署
减小服务端存储压力
说明:刘宏缔的架构森林是一个专注架构的博客,
网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/25/springbootspringsecurity-zheng-he-jwt-shi-xian-deng-lu-he-quan-xian-yan-zheng-springboot233/
对应的源码可以访问这里获取: 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)