JWT
1.介绍:
全称 JSON Web Token
,通过数字签名的方式,以JSON
为载体,在不同的服务终端之间安全的传递信息。
常用于授权认证,用户登录后的每个请求都包含JWT
,后端处理请求之前都要进行校验。
2.组成:
Header
:数据头,令牌类型和加密算法
Payload
:负载,请求体和其他数据
Signature
:签名,把头部的base64UrlEncode与负载的base64UrlEncode拼接起来再进行HMACSHA256加密
用户认证流程
1.用户提交登录表单(用户名和密码)
2.后端校验成功后生成JWT,通过response的header返回给前端
3.前端将
JWT
保存到LocalStorage
中4.之后所有的请求中请求头都携带
JWT
进行身份认证
Spring Security(安全框架)
1、介绍
Spring Security
是一个能够为基于Spring
的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
如果项目中需要进行权限管理,具有多个角色和多种权限,我们可以使用Spring Security。
采用的是责任链的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链。
2、功能
Authentication
(认证),就是用户登录
Authorization
(授权),判断用户拥有什么权限,可以访问什么资源
安全防护,跨站脚本攻击,session
攻击等
非常容易结合Spring
进行使用
3、Spring Security
与Shiro
的区别
优点:
1、Spring Security基于Spring开发,项目如果使用Spring作为基础,配合Spring Security做权限更加方便。而Shiro需要和Spring进行整合开发
2、Spring Security功能比Shiro更加丰富,例如安全防护方面
3、Spring Security社区资源相对比Shiro更加丰富
缺点:
1)Shiro的配置和使用比较简单,Spring Security上手复杂些
2)Shiro依赖性低,不需要依赖任何框架和容器,可以独立运行。Spring Security依赖Spring容器
需要实现的过滤器和处理器
1、LogoutSuccessHandler:
表示登出处理器
2、验证码过滤器Filter
3、登录认证成功、失败处理器
4、BasicAuthenticationFilter:
该过滤器用于普通http请求进行身份认证
5、AuthenticationEntryPoint:
表示认证失败处理器
6、AccessDenieHandler:
用户发起无权限访问请求的处理器
7、UserServiceDatils 接口:
该接口十分重要,用于从数据库中验证用户名密码
8、PasswordEncoder密码验证器
整合
1.添加相应依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.3</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.11</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.15</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
2.写一个JWT工具类(生成JWT、解析JWT、判断JWT是否过期)
import io.jsonwebtoken.*;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;import java.util.Date;@Data@Component@ConfigurationProperties(prefix = "xiaolinbao.jwt")public class JwtUtils {//使用@ConfigurationProperties注解可以读取配置文件中的信息,只要在 Bean 上添加上了这个注解,指定好配置文件中的前缀,那么对应的配置文件数据就会自动填充到 Bean 的属性中private long expire;private String secret;private String header;// 生成JWTpublic String generateToken(String username) {Date nowDate = new Date();Date expireDate = new Date(nowDate.getTime() + 1000 * expire);return Jwts.builder().setHeaderParam("typ", "JWT").setSubject(username).setIssuedAt(nowDate).setExpiration(expireDate)// 7天过期.signWith(SignatureAlgorithm.HS512, secret).compact();}// 解析JWTpublic Claims getClaimsByToken(String jwt) {try {return Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();} catch (Exception e) {return null;}}// 判断JWT是否过期public boolean isTokenExpired(Claims claims) {return claims.getExpiration().before(new Date());}}
#JWT配置xiaolinbao:jwt:header: Authorizationexpire: 604800 # 7天,s为单位secret: abcdefghabcdefghabcdefghabcdefgh
封装Result
import lombok.Data;import java.io.Serializable;@Datapublic class Result implements Serializable {private int code;private String msg;private Object data;public static Result succ(Object data) {return succ(200, "操作成功", data);}public static Result fail(String msg) {return fail(400, msg, null);}public static Result succ (int code, String msg, Object data) {Result result = new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}public static Result fail (int code, String msg, Object data) {Result result = new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}}
LoginSuccessHandler(登录成功处理器)实现AuthenticationSuccessHandler
@Componentpublic class LoginSuccessHandler implements AuthenticationSuccessHandler {@AutowiredJwtUtils jwtUtils;@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();// 生成JWT,并放置到请求头中String jwt = jwtUtils.generateToken(authentication.getName());httpServletResponse.setHeader(jwtUtils.getHeader(), jwt);Result result = Result.succ("SuccessLogin");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
LoginFailureHandler(登录失败处理器)实现AuthenticationFailureHandler
@Componentpublic class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();String errorMessage = "用户名或密码错误";Result result;if (e instanceof CaptchaException) {errorMessage = "验证码错误";result = Result.fail(errorMessage);} else {result = Result.fail(errorMessage);}outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
自定义的验证码异常
public class CaptchaException extends AuthenticationException {public CaptchaException(String msg) {super(msg);}}
验证码工具类
@Configurationpublic class KaptchaConfig {@BeanDefaultKaptcha producer() {Properties properties = new Properties();properties.put("kaptcha.border", "no");properties.put("kaptcha.textproducer.font.color", "black");properties.put("kaptcha.textproducer.char.space", "4");properties.put("kaptcha.image.height", "40");properties.put("kaptcha.image.width", "120");properties.put("kaptcha.textproducer.font.size", "30");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}}
验证码 Controller
@AutowiredProducer producer;@GetMapping("/captcha")public Result Captcha() throws IOException {String key = UUID.randomUUID().toString();String code = producer.createText();BufferedImage image = producer.createImage(code);ByteArrayOutputStream outputStream = new ByteArrayOutputStream();ImageIO.write(image, "jpg", outputStream);BASE64Encoder encoder = new BASE64Encoder();String str = "data:image/jpeg;base64,";String base64Img = str + encoder.encode(outputStream.toByteArray());//随机码为key,验证码为valueredisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);return Result.succ(MapUtil.builder().put("userKey", key).put("captcherImg", base64Img).build());}
验证码过滤器CaptchaFilter
@Componentpublic class CaptchaFilter extends OncePerRequestFilter {@AutowiredRedisUtil redisUtil;@AutowiredLoginFailureHandler loginFailureHandler;//自定义处理逻辑@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {String url = httpServletRequest.getRequestURI();if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {// 校验验证码try {validate(httpServletRequest);} catch (CaptchaException e) {// 交给认证失败处理器loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);}}filterChain.doFilter(httpServletRequest, httpServletResponse);}// 校验验证码逻辑private void validate(HttpServletRequest httpServletRequest) {String code = httpServletRequest.getParameter("code");String key = httpServletRequest.getParameter("userKey");if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {throw new CaptchaException("验证码错误");}if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {throw new CaptchaException("验证码错误");}// 若验证码正确,执行以下语句// 一次性使用redisUtil.hdel(Const.CAPTCHA_KEY, key);}}
JWT过滤器JwtAuthenticationFilter
检验JWT是否正确以及是否过期
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {@AutowiredJwtUtils jwtUtils;@AutowiredUserDetailServiceImpl userDetailService;@AutowiredSysUserService sysUserService;public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {String jwt = request.getHeader(jwtUtils.getHeader());// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口if (StrUtil.isBlankOrUndefined(jwt)) {chain.doFilter(request, response);return;}Claims claim = jwtUtils.getClaimsByToken(jwt);if (claim == null) {throw new JwtException("token 异常");}if (jwtUtils.isTokenExpired(claim)) {throw new JwtException("token 已过期");}String username = claim.getSubject();// 获取用户的权限等信息SysUser sysUser = sysUserService.getByUsername(username);// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));SecurityContextHolder.getContext().setAuthentication(token);chain.doFilter(request, response);}}
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
等方法获取到当前登录的用户信息
JWT认证失败处理器JwtAuthenticationEntryPoint
@Componentpublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {//认证失败的处理@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);ServletOutputStream outputStream = httpServletResponse.getOutputStream();Result result = Result.fail("请先登录");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
从数据库中验证用户名、密码:UserServiceDetails、AuthenticationManager、UserDetails
SpringSecurity中的认证管理器AuthenticationManager是一个抽象接口,用以提供各种认证方式。一般我们都使用从数据库中验证用户名、密码是否正确这种认证方式。
AuthenticationManager的默认实现类是ProviderManager,ProviderManager提供很多认证方式,DaoAuthenticationProvider是AuthenticationProvider的一种实现,可以通过实现UserDetailsService接口的方式来实现数据库查询方式登录。
Spring Security在拿到UserDetails之后,会去对比Authentication(Authentication如何得到?我们使用的是默认的UsernamePasswordAuthenticationFilter,它会读取表单中的用户信息并生成Authentication),若密码正确,则Spring Secuity自动帮忙完成登录
定义一个UserDetails接口的实现类,称为AccountUser实现所有方法
public interface UserDetails extends Serializable {//获取用户权限Collection<? extends GrantedAuthority> getAuthorities();//用户密码String getPassword();//用户名String getUsername();//用户是否过期boolean isAccountNonExpired();//用户是否被锁定boolean isAccountNonLocked();//认证信息是否过期boolean isCredentialsNonExpired();//用户启用还是禁用boolean isEnabled();}
实现 UserDetails (默认有权限管理功能)
public class AccountUser implements UserDetails {private Long userId;private static final long serialVersionUID = 540L;private static final Log logger = LogFactory.getLog(User.class);private String password;private final String username;private final Collection<? extends GrantedAuthority> authorities;private final boolean accountNonExpired;private final boolean accountNonLocked;private final boolean credentialsNonExpired;private final boolean enabled;public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {this(userId, username, password, true, true, true, true, authorities);}public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");this.userId = userId;this.username = username;this.password = password;this.enabled = enabled;this.accountNonExpired = accountNonExpired;this.credentialsNonExpired = credentialsNonExpired;this.accountNonLocked = accountNonLocked;this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}@Overridepublic boolean isAccountNonExpired() {return this.accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return this.accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return this.credentialsNonExpired;}@Overridepublic boolean isEnabled() {return this.enabled;}}
实现 UserDetailsService 重写其loadUserByUsername方法 使用用户名在数据库中查找用户信息返回
@Servicepublic class UserDetailServiceImpl implements UserDetailsService {@AutowiredSysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser = sysUserService.getByUsername(username);if (sysUser == null) {throw new UsernameNotFoundException("用户名或密码错误");}return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));}/** * 获取用户权限信息(角色、菜单权限) * @param userId * @return */public List<GrantedAuthority> getUserAuthority(Long userId) {// 实际怎么写以数据表结构为准,这里只是写个例子// 角色(比如ROLE_admin),菜单操作权限(比如sys:user:list)String authority = sysUserService.getUserAuthorityInfo(userId); // 比如ROLE_admin,ROLE_normal,sys:user:list,...return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);}}
实现了上述几个接口,从数据库中验证用户名、密码的过程将由框架帮我们完成,封装隐藏了
无权限访问的处理:AccessDenieHandler
当权限不足时,我们需要设置权限不足状态码403,并将错误信息返回给前端
@Componentpublic class JwtAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = httpServletResponse.getOutputStream();Result result = Result.fail(e.getMessage());outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
登出处理器 LogoutSuccessHandler
1.将原来的 JWT 置为空返给前端
2.用空字符串覆盖之前的 JWT (JWT是无状态 无法销毁 只能等过期 所以采用置空浏览器中保存的JWT)
3.清除SecurityContext中的用户信息 (通过创建SecurityContextLogoutHandler对象,调用它的logout方法)
实现 LogoutSuccessHandler 重写 onLogoutSuccess 方法
@Componentpublic class JWTLogoutSuccessHandler implements LogoutSuccessHandler {@AutowiredJwtUtils jwtUtils;@Overridepublic void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {if (authentication != null) {new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);}httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();httpServletResponse.setHeader(jwtUtils.getHeader(), "");Result result = Result.succ("SuccessLogout");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
密码加密解密:PasswordEncoder
1.首先前端对密码进行esa加密
2.后端对前端传输过来的密码进行解密
3.再根据数据库的加密规则BCrypt进行加密
SpringSecurity提供了用于密码加密解密的工具类BCryptPasswordEncoder
需自定义PasswordEncoder类,并使其继承BCryptPasswordEncoder,重写其matches方法
@NoArgsConstructorpublic class PasswordEncoder extends BCryptPasswordEncoder {//判断从前端接收的密码与数据库中的密码是否一致@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {// 接收到的前端的密码String pwd = rawPassword.toString();// 进行rsa解密try {pwd = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, pwd);} catch (Exception e) {throw new BadCredentialsException(e.getMessage());}if (encodedPassword != null && encodedPassword.length() != 0) {return BCrypt.checkpw(pwd, encodedPassword);} else {return false;}}}
Spring Security全局配置:SecurityConfig
需要继承WebSecurityConfigurerAdapter(采用适配器模式,继承后SecurityConfig可以看做是WebSecurityConfigurer)
SecurityConfig需要使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
注解
Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限。prePostEnabled = true即可在方法前后进行权限检查
Security内置的权限注解如下:
@PreAuthorize:方法执行前进行权限检查@PreAuthorize("hasAuthority('sys:user:list')")
@PostAuthorize:方法执行后进行权限检查
@Secured:类似于 @PreAuthorize
可以在Controller的方法前添加这些注解表示接口需要什么权限。
配置类还需使用@EnableWebSecurity注解,该注解有两个作用:1. 加载了WebSecurityConfiguration配置类, 配置安全认证策略。2.加载了AuthenticationConfiguration, 配置了认证信息。AuthenticationConfiguration这个类的作用就是用来创建ProviderManager。
@EnableWebSecurity完成的工作便是加载了WebSecurityConfiguration,AuthenticationConfiguration这两个核心配置类,也就此将spring security的职责划分为了配置安全信息,配置认证信息两部分。
在SecurityConfig这个配置类中,我们需要将之前写的拦截器和处理器都autowire进来,并使用@Bean注解,声明JwtAuthenticationFilter和PasswordEncoder的构造函数。在JwtAuthenticationFilter的构造函数中,我们调用authenticationManager()方法给JwtAuthenticationFilter提供AuthenticationManager。
配置类需要重写configure方法进行配置,该方法有多种重载形式,我们使用其中的两种,其中一个用于配置url安全拦截配置,另一个用于AuthenticationManager配置UserDetailsService的实现类
@Configuration@EnableWebSecurity@RequiredArgsConstructor@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredLoginFailureHandler loginFailureHandler;@AutowiredLoginSuccessHandler loginSuccessHandler;@AutowiredCaptchaFilter captchaFilter;@AutowiredJwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;@AutowiredJwtAccessDeniedHandler jwtAccessDeniedHandler;@AutowiredUserDetailServiceImpl userDetailService;@AutowiredJWTLogoutSuccessHandler jwtLogoutSuccessHandler;@BeanJwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());return jwtAuthenticationFilter;}private static final String[] URL_WHITELIST = {"/login","/logout","/captcha","/favicon.ico"};@BeanPasswordEncoder PasswordEncoder() {return new PasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable()// 登录配置.formLogin().successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).and().logout().logoutSuccessHandler(jwtLogoutSuccessHandler)// 禁用session.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 配置拦截规则.and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll().anyRequest().authenticated()// 异常处理器.and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler)// 配置自定义的过滤器.and().addFilter(jwtAuthenticationFilter())// 验证码过滤器放在UsernamePassword过滤器之前.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailService);}}
自定义权限校验注解
Spring Security
提供了Spring EL
表达式,允许我们在定义接口访问的方法上面添加注解,来控制访问权限。
@PreAuthorize 注解用于配置接口要求用户拥有某些权限才可访问
方法 | 参数 | 描述 |
---|---|---|
hasPermi | String | 验证用户是否具备某权限 |
lacksPermi | String | 验证用户是否不具备某权限,与 hasPermi逻辑相反 |
hasAnyPermi | String | 验证用户是否具有以下任意一个权限 |
hasRole | String | 判断用户是否拥有某个角色 |
lacksRole | String | 验证用户是否不具备某角色,与 isRole逻辑相反 |
hasAnyRoles | String | 验证用户是否具有以下任意一个角色,多个逗号分隔 |
使用 @ss
代表 PermissionService(许可服务) 类,对每个接口拦截并调用PermissionService
的对应方法判断接口调用者的权限。
package com.example.framework.web.service;import java.util.Set;import org.springframework.stereotype.Service;import org.springframework.util.CollectionUtils;import com.example.common.core.domain.entity.SysRole;import com.example.common.core.domain.model.LoginUser;import com.example.common.utils.SecurityUtils;import com.example.common.utils.StringUtils;import com.example.framework.security.context.PermissionContextHolder;/** * 自定义权限实现,ss => SpringSecurity首字母 * 超级管理员拥有所有权限,不受权限约束。 */@Service("ss")public class PermissionService{/** 所有权限标识 */private static final String ALL_PERMISSION = "*:*:*";/** 管理员角色权限标识 */private static final String SUPER_ADMIN = "admin";private static final String ROLE_DELIMETER = ",";private static final String PERMISSION_DELIMETER = ",";/** * 验证用户是否具备某权限 ** @param permission 权限字符串 * @return 用户是否具备某权限 */public boolean hasPermi(String permission){if (StringUtils.isEmpty(permission)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permission);return hasPermissions(loginUser.getPermissions(), permission);}/** * 验证用户是否不具备某权限,与 hasPermi逻辑相反 * * @param permission 权限字符串 * @return 用户是否不具备某权限 */public boolean lacksPermi(String permission){return hasPermi(permission) != true;}/** * 验证用户是否具有以下任意一个权限 * * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表 * @return 用户是否具有以下任意一个权限 */public boolean hasAnyPermi(String permissions){if (StringUtils.isEmpty(permissions)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permissions);Set<String> authorities = loginUser.getPermissions();for (String permission : permissions.split(PERMISSION_DELIMETER)){if (permission != null && hasPermissions(authorities, permission)){return true;}}return false;}/** * 判断用户是否拥有某个角色 ** @param role 角色字符串 * @return 用户是否具备某角色 */public boolean hasRole(String role){if (StringUtils.isEmpty(role)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (SysRole sysRole : loginUser.getUser().getRoles()){String roleKey = sysRole.getRoleKey();if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))){return true;}}return false;}/** * 验证用户是否不具备某角色,与 isRole逻辑相反。 * * @param role 角色名称 * @return 用户是否不具备某角色 */public boolean lacksRole(String role){return hasRole(role) != true;}/** * 验证用户是否具有以下任意一个角色 * * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 * @return 用户是否具有以下任意一个角色 */public boolean hasAnyRoles(String roles){if (StringUtils.isEmpty(roles)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (String role : roles.split(ROLE_DELIMETER)){if (hasRole(role)){return true;}}return false;}/** * 判断是否包含权限 ** @param permissions 权限列表 * @param permission 权限字符串 * @return 用户是否具备某权限 */private boolean hasPermissions(Set<String> permissions, String permission){return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}}
数据权限示例
// 符合system:user:list权限要求@PreAuthorize("@ss.hasPermi('system:user:list')")// 不符合system:user:list权限要求@PreAuthorize("@ss.lacksPermi('system:user:list')")// 符合system:user:add或system:user:edit权限要求即可@PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")
角色权限示例
// 属于user角色@PreAuthorize("@ss.hasRole('user')")// 不属于user角色@PreAuthorize("@ss.lacksRole('user')")// 属于user或者admin之一@PreAuthorize("@ss.hasAnyRoles('user,admin')")
公开接口(不需要验证权限可以公开访问的)
使用注解方式,只需要在Controller
的类或方法上加入@Anonymous
该注解即可
// @PreAuthorize("@ss.xxxx('....')") 注释或删除掉原有的权限注解@Anonymous@GetMapping("/list")public List<SysXxxx> list(SysXxxx xxxx){return xxxxList;}
前端
前端需要做两件事,一是登录成功后把JWT存到localStore里面,二是在每次请求之前,都在请求头中添加JWT
我们在store文件夹里创建index.js,将JWT定义为token,以及定义SET_TOKEN方法
Vue.use(Vuex)export default new Vuex.Store({state: {token: ''},mutations: {SET_TOKEN: (state, token) => {state.token = tokenlocalStorage.setItem("token", token)},},actions: {},modules: {}})
在登录成功时,接收后端传来的JWT并保存
const jwt = res.headers['authorization']this.$store.commit('SET_TOKEN', jwt)
在src文件夹下创建axios.js,进行axios配置,配置前置拦截器,为所有需要权限的请求装配上header的token信息
const request = axios.create({timeout: 5000,headers: {'Content-Type': "application/json; charset=utf-8"}})// 前置拦截,为所有需要权限的请求装配上header的token信息request.interceptors.request.use(config => {config.headers['Authorization'] = localStorage.getItem("token")return config})