赞
踩
修改自前文,十分贴近公司开发的生产环境
前后端分离 SpringBoot + SpringSecurity 权限解决方案
RBAC(Role-Based Access Control,基于角色的访问控制)
代码已经放在 github 上了:https://github.com/larger5/SpringBoot_SpringSecurity_JWT_RBAC.git
<!--安全框架--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--JSON封装--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.36</version> </dependency> <!--JWT--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
package com.cun.security3.bean; import java.io.Serializable; public class AjaxResponseBody implements Serializable{ private String status; private String msg; private Object result; private String jwtToken; public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } public String getJwtToken() { return jwtToken; } public void setJwtToken(String jwtToken) { this.jwtToken = jwtToken; } }
package com.cun.security3.config; import com.alibaba.fastjson.JSON; import com.cun.security3.bean.AjaxResponseBody; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { AjaxResponseBody responseBody = new AjaxResponseBody(); responseBody.setStatus("300"); responseBody.setMsg("Need Authorities!"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }
package com.cun.security3.config; import com.alibaba.fastjson.JSON; import com.cun.security3.bean.AjaxResponseBody; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { AjaxResponseBody responseBody = new AjaxResponseBody(); responseBody.setStatus("000"); responseBody.setMsg("Need Authorities!"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }
package com.cun.security3.config; import com.alibaba.fastjson.JSON; import com.cun.security3.bean.AjaxResponseBody; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { AjaxResponseBody responseBody = new AjaxResponseBody(); responseBody.setStatus("400"); responseBody.setMsg("Login Failure!"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }
package com.cun.security3.config; import com.alibaba.fastjson.JSON; import com.cun.security3.bean.AjaxResponseBody; import com.cun.security3.utils.JwtTokenUtil; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { AjaxResponseBody responseBody = new AjaxResponseBody(); responseBody.setStatus("200"); responseBody.setMsg("Login Success!"); SelfUserDetails userDetails = (SelfUserDetails) authentication.getPrincipal(); String jwtToken = JwtTokenUtil.generateToken(userDetails.getUsername(), 300, "_secret"); responseBody.setJwtToken(jwtToken); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }
package com.cun.security3.config; import com.alibaba.fastjson.JSON; import com.cun.security3.bean.AjaxResponseBody; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { AjaxResponseBody responseBody = new AjaxResponseBody(); responseBody.setStatus("100"); responseBody.setMsg("Logout Success!"); httpServletResponse.getWriter().write(JSON.toJSONString(responseBody)); } }
package com.cun.security3.config; import com.cun.security3.utils.JwtTokenUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired SelfUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { final String authToken = authHeader.substring("Bearer ".length()); String username = JwtTokenUtil.parseToken(authToken, "_secret"); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
package com.cun.security3.config; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import javax.servlet.http.HttpServletRequest; import java.util.HashSet; import java.util.Set; @Component("rbacauthorityservice") public class RbacAuthorityService { public boolean hasPermission(HttpServletRequest request, Authentication authentication) { Object userInfo = authentication.getPrincipal(); boolean hasPermission = false; if (userInfo instanceof UserDetails) { String username = ((UserDetails) userInfo).getUsername(); //获取资源 Set<String> urls = new HashSet(); urls.add("/common/**"); // 这些 url 都是要登录后才能访问,且其他的 url 都不能访问! Set set2 = new HashSet(); Set set3 = new HashSet(); AntPathMatcher antPathMatcher = new AntPathMatcher(); for (String url : urls) { if (antPathMatcher.match(url, request.getRequestURI())) { hasPermission = true; break; } } return hasPermission; } else { return false; } } }
package com.cun.security3.config; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serializable; import java.util.Collection; import java.util.Set; /** * ① 定义 user 对象 */ public class SelfUserDetails implements UserDetails, Serializable { private String username; private String password; private Set<? extends GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } public void setAuthorities(Set<? extends GrantedAuthority> authorities) { this.authorities = authorities; } @Override public String getPassword() { // 最重点Ⅰ return this.password; } @Override public String getUsername() { // 最重点Ⅱ return this.username; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
package com.cun.security3.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.Set; @Component public class SelfUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //构建用户信息的逻辑(取数据库/LDAP等用户信息) SelfUserDetails userInfo = new SelfUserDetails(); userInfo.setUsername(username); userInfo.setPassword(new BCryptPasswordEncoder().encode("123")); Set authoritiesSet = new HashSet(); GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN"); authoritiesSet.add(authority); userInfo.setAuthorities(authoritiesSet); return userInfo; } }
package com.cun.security3.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SpringSecurityConf extends WebSecurityConfigurerAdapter { @Autowired AjaxAuthenticationEntryPoint authenticationEntryPoint; // 未登陆时返回 JSON 格式的数据给前端(否则为 html) @Autowired AjaxAuthenticationSuccessHandler authenticationSuccessHandler; // 登录成功返回的 JSON 格式数据给前端(否则为 html) @Autowired AjaxAuthenticationFailureHandler authenticationFailureHandler; // 登录失败返回的 JSON 格式数据给前端(否则为 html) @Autowired AjaxLogoutSuccessHandler logoutSuccessHandler; // 注销成功返回的 JSON 格式数据给前端(否则为 登录时的 html) @Autowired AjaxAccessDeniedHandler accessDeniedHandler; // 无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面) @Autowired SelfUserDetailsService userDetailsService; // 自定义user @Autowired JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 拦截器 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 加入自定义的安全认证 auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { // 去掉 CSRF http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 使用 JWT,关闭token .and() .httpBasic().authenticationEntryPoint(authenticationEntryPoint) .and() .authorizeRequests() .anyRequest() .access("@rbacauthorityservice.hasPermission(request,authentication)") // RBAC 动态 url 认证 .and() .formLogin() //开启登录 .successHandler(authenticationSuccessHandler) // 登录成功 .failureHandler(authenticationFailureHandler) // 登录失败 .permitAll() .and() .logout() .logoutSuccessHandler(logoutSuccessHandler) .permitAll(); // 记住我 http.rememberMe().rememberMeParameter("remember-me") .userDetailsService(userDetailsService).tokenValiditySeconds(300); http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权访问 JSON 格式的数据 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // JWT Filter } }
package com.cun.security3.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.io.InputStream; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Date; public class JwtTokenUtil { private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("jwt.jks"); // 寻找证书文件 private static PrivateKey privateKey = null; private static PublicKey publicKey = null; static { // 将证书文件里边的私钥公钥拿出来 try { KeyStore keyStore = KeyStore.getInstance("JKS"); // java key store 固定常量 keyStore.load(inputStream, "123456".toCharArray()); privateKey = (PrivateKey) keyStore.getKey("jwt", "123456".toCharArray()); // jwt 为 命令生成整数文件时的别名 publicKey = keyStore.getCertificate("jwt").getPublicKey(); } catch (Exception e) { e.printStackTrace(); } } public static String generateToken(String subject, int expirationSeconds, String salt) { return Jwts.builder() .setClaims(null) .setSubject(subject) .setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000)) // .signWith(SignatureAlgorithm.HS512, salt) // 不使用公钥私钥 .signWith(SignatureAlgorithm.RS256, privateKey) .compact(); } public static String parseToken(String token, String salt) { String subject = null; try { Claims claims = Jwts.parser() // .setSigningKey(salt) // 不使用公钥私钥 .setSigningKey(publicKey) .parseClaimsJws(token).getBody(); subject = claims.getSubject(); } catch (Exception e) { } return subject; } }
不足 | 解决 |
---|---|
没有注销 token 登陆 | 每次登陆,生成 token 放到 Redis 数据库里边,调用接口的时候,先查有没有这个 token,注销时把 token 删除 |
由于网友对 jwt
的使用方式存在疑问,这里更新一下测试方法,
借助了 postman
,每次请求都是 ajax
方式,注意 get/post
等类型
①注意是 post
请求,请求 /login
,存好返回给你的 jwtToken
,以后每次请求都要带上它
① 注意任何请求要带上 jwtToken
,不像之前的基于 Session
,一登录成功就完事。
② 若还是请求失败,看 RbacAuthorityService
是否开放了该 url
。
实际开发中,没有必要引入 jjwt,token 存于 Redis 即可(自带有效期),性能更好。
其实本文代码真的写得很差,详细设计见下面链接:
高可用的接口安全规范
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。