然后接着讲解决授权
解决授权
问题1:我们是在哪里赋予用户权限的?有两个地方:
- 1、用户登录,调用调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。
- 2、接口调用进行身份认证过滤器时候JWTAuthenticationFilter,需要返回用户权限信息
问题2:在哪里决定什么接口需要什么权限?
Security内置的权限注解:
- @PreAuthorize:方法执行前进行权限检查
- @PostAuthorize:方法执行后进行权限检查
- @Secured:类似于 @PreAuthorize
可以在Controller的方法前添加这些注解表示接口需要什么权限。
比如需要Admin角色权限:
@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限
@PreAuthorize("hasAuthority('sys:user:save')")
流程清晰之后我们就开始我们的编码:
- UserDetailsServiceImpl
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
...
return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
}
public List<GrantedAuthority> getUserAuthority(Long userId) {
// 通过内置的工具类,把权限字符串封装成GrantedAuthority列表
return AuthorityUtils.commaSeparatedStringToAuthorityList(
sysUserService.getUserAuthorityInfo(userId)
);
}
com.rao.security.JWTAuthenticationFilter
SysUser sysUser = sysUserService.getByUsername(username);
List<GrantedAuthority> grantedAuthorities = userDetailsService.getUserAuthority(sysUser.getId());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities);
代码中的com.rao.service.impl.SysUserServiceImpl#getUserAuthorityInfo是重点:
package com.rao.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rao.entity.SysMenu;
import com.rao.entity.SysRole;
import com.rao.entity.SysUser;
import com.rao.mapper.SysUserMapper;
import com.rao.service.SysMenuService;
import com.rao.service.SysRoleService;
import com.rao.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rao.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 服务实现类
* </p>
*
* @author 饶小兵
* @since 2022-03-09
*/
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Autowired
SysRoleService sysRoleService;
@Autowired
RedisUtil redisUtil;
@Autowired
SysMenuService sysMenuService;
@Autowired
SysUserMapper sysUserMapper;
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username", username));
}
public String getUserAuthoriyInfo(Long id) {
SysUser sysUser = this.getById(id);
String authority=null;
if (redisUtil.hasKey("GrantedAuthority:"+sysUser.getUsername())){
authority = (String) redisUtil.get("GrantedAuthority" + sysUser.getUsername());
}else{
List<SysRole> roles = sysRoleService.list
(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + id));
if (roles.size()>0){
String roleNames = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
authority=roleNames.concat(",");
}
List<Long> MenuIds = sysUserMapper.getNavMenuIds(id);
if (MenuIds.size()>0){
List<SysMenu> sysMenus = sysMenuService.listByIds(MenuIds);
String permName = sysMenus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority=authority.concat(permName);
}
log.info("用户ID-{}--拥有的权限:{}",id,authority);
redisUtil.set("GrantedAuthority"+sysUser.getUsername(),authority,60*60);
}
return authority;
}
@Override
public void clearUserAuthorityInfo(String username) {
redisUtil.del("GrantedAuthority"+username);
}
@Override
public void clearUserAuthorityInfoByRoleId(Long roleId) {
List<SysUser> sysUsers = this.list(new QueryWrapper<SysUser>().inSql("id", "select user_id from sys_user_role where role_id = " + roleId));
sysUsers.forEach(u->{
this.clearUserAuthorityInfo(u.getUsername());
});
}
//
@Override
public void clearUserAuthorityInfoByMenuId(Long menuId) {
List<SysUser> sysUsers = sysUserMapper.listByMenuId(menuId);
sysUsers.forEach(u->{
this.clearUserAuthorityInfo(u.getUsername());
});
}
}
可以看到,我通过用户id分别获取到用户的角色信息和菜单信息,然后通过逗号链接起来,因为角色信息我们需要这样“ROLE_”+角色,所以才有了上面的写法:
比如用户拥有Admin角色和添加用户权限,则最后的字符串是:ROLE_admin,syssave。
同时为了避免多次查库,我做了一层缓存,这里理解应该不难。
然后sysUserMapper.getNavMenuIds(userId)因为要查询数据库,具体SQL如下:
- com.rao.mapper.SysUserMapper#getNavMenuIds
<select id="getNavMenuIds" resultType="java.lang.Long">
SELECT
DISTINCT rm.menu_id
FROM
sys_user_role ur
LEFT JOIN `sys_role_menu` rm ON rm.role_id = ur.role_id
WHERE
ur.user_id = #{userId};
</select>
上面表示通过用户ID获取用户关联的菜单的id,因此需要用到两个中间表的关联了。
ok,这样我们就赋予了用户角色和操作权限了。后面我们只需要在Controller添加上具体注解表示需要的权限,Security就会自动帮我们自动完成权限校验了。
因为上面我在获取用户权限那里添加了个缓存,这时候问题来了,就是权限缓存的实时更新问题,比如当后台更新某个管理员的权限角色信息的时候如果权限缓存信息没有实时更新,就会出现操作无效的问题,那么我们现在点定义几个方法,用于清除某个用户或角色或者某个菜单的权限的方法:
上面最后一个方法查到了与菜单关联的所有用户的,具体sql如下:
- com.rao.mapper.SysUserMapper#listByMenuId
<select id="listByMenuId" resultType="com.javacat.entity.SysUser">
SELECT
DISTINCT
su.*
FROM
sys_user_role ur
LEFT JOIN `sys_role_menu` rm ON rm.role_id = ur.role_id
LEFT JOIN `sys_user` su ON su.id = ur.user_id
WHERE
rm.menu_id = #{menuId};
</select>
退出数据返回
jwt -username
token - 随机码 - redis
- com.rao.security.JwtLogoutSuccessHandler
package com.rao.security;
import cn.hutool.json.JSONUtil;
import com.rao.common.lang.Result;
import com.rao.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication!=null){
new SecurityContextLogoutHandler().logout(request,response,authentication);
}
response.setContentType("application/json;charset=UTF-8");
response.setHeader(jwtUtils.getHeader(),"");
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
无权限数据返回
- com.rao.security.JwtAccessDeniedHandler
package com.rao.security;
import cn.hutool.json.JSONUtil;
import com.rao.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
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.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("权限不够");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail(accessDeniedException.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
解决跨域问题
上面的调试我们都是使用的postman,如果我们和前端进行对接的时候,会出现跨域的问题,如何解决?
- com.rao.config.CorsConfig
package com.rao.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
到了这里我们对用户和权限分配写好了下节就讲具体实现了相当于前面是搭后端框架