用户授权
总体思路
- 进行登录,登录时根据用户名(用户id)查询用户可以操作的权限。把用户可以操作的权限放入Redis中(key:username,value:操作权限数据)
- 校验时,从请求头获取token字符串,从token中获取username。用username到redis中查询权限数据。
具体实现思路
- 修改loadUserByUsername接口方法。根据用户名(id)查询用户操作的权限数据,封装返回。
- 修改TokenLoginFilter。增加权限数据部分
获取当前登录用户的权限数据,把数据放入Redis中(key:username,value:权限数据) - 修改TokenAuthenticationFilter。
从请求头获取token,从token获取username,从username到redis中获取权限数据 - 修改配置类,添加Redis
- 在controller中添加权限判断注解
具体实现
1. 修改loadUserByUsername接口方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService userService;
@Autowired
private SysMenuService menuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户
SysUser user = userService.getUserByUserName(username); //需要在SysUserService中具体实现功能
if(null == user) {
throw new UsernameNotFoundException("用户名不存在!");
}
if(user.getStatus().intValue() == 0) {
throw new RuntimeException("账号已停用");
}
//根据用户id查询用户操作权限数据
List<String> buttonList = menuService.getButtonByUserId(user.getId());
//把权限数据封装成需要的格式
List<SimpleGrantedAuthority> authList = new ArrayList<>();
for (String button : buttonList) {
authList.add(new SimpleGrantedAuthority(button.trim()));
}
return new CustomUser(user, authList);
}
}
2. 修改TokenLoginFilter
先添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private RedisTemplate redisTemplate; //引入redis
//构造方法
public TokenLoginFilter(AuthenticationManager authenticationManager,RedisTemplate redisTemplate) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口及提交方式,可以指定任意路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
this.redisTemplate = redisTemplate; //注入redis
}
//登录认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//登录成功
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
CustomUser customUser = (CustomUser) authResult.getPrincipal();
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
//把权限数据保存在redis中,key:username,value:权限数据
redisTemplate.opsForValue().set(customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities()));
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
if(failed.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, 204, failed.getMessage()));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeNum.LOGIN_ERROR));
}
}
}
3. 修改TokenAuthenticationFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//如果是登录接口,直接放行
if("/admin/system/index/login".equals(httpServletRequest.getRequestURI())) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(httpServletRequest);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
ResponseUtil.out(httpServletResponse, Result.build(null, ResultCodeNum.PERMISSION));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
logger.info("token:"+token);
if (!StringUtils.isEmpty(token)) {
String useruame = JwtHelper.getUsername(token);
logger.info("useruame:"+useruame);
if (!StringUtils.isEmpty(useruame)) {
//通过username从redis中获取权限数据
String authString = (String) redisTemplate.opsForValue().get(useruame);
//把redis中字符串类型的数据转换为所需的集合数据类型List<SimpleGrantedAuthority>
if (!StringUtils.isEmpty(authString)){
List<Map> mapList = JSON.parseArray(authString, Map.class);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Map map : mapList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority((String)(map.get("authority")));
authorities.add(authority);
}
return new UsernamePasswordAuthenticationToken(useruame, null, authorities);
}else {
return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
}
}
}
return null;
}
}
4. 修改配置类
修改WebSecurityConfig类
配置类添加注解:
开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
添加注入bean:
@Autowired
private RedisTemplate redisTemplate;
添加参数:两个fillter添加redisTemplate参数
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解功能,默认禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Autowired
private RedisTemplate redisTemplate;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
//.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
application.yml添加redis配置
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 1800000
password:
jedis:
pool:
max-active: 20 #最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示没限制)
max-idle: 5 #最大空闲
min-idle: 0 #最小空闲
5. 在controller中添加权限判断注解
Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限
通过@PreAuthorize标签控制controller层接口权限
public class SysRoleController {
@Autowired
private SysRoleService sysRoleService;
@PreAuthorize("hasAuthority('bnt.sysRole.list')")
@ApiOperation(value = "获取分页列表")
@GetMapping("{page}/{limit}")
public Result index(
@ApiParam(name = "page", value = "当前页码", required = true)
@PathVariable Long page,
@ApiParam(name = "limit", value = "每页记录数", required = true)
@PathVariable Long limit,
@ApiParam(name = "roleQueryVo", value = "查询对象", required = false)
SysRoleQueryVo roleQueryVo) {
Page<SysRole> pageParam = new Page<>(page, limit);
IPage<SysRole> pageModel = sysRoleService.selectPage(pageParam, roleQueryVo);
return Result.ok(pageModel);
}
@PreAuthorize("hasAuthority('bnt.sysRole.list')")
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
SysRole role = sysRoleService.getById(id);
return Result.ok(role);
}
@PreAuthorize("hasAuthority('bnt.sysRole.add')")
@ApiOperation(value = "新增角色")
@PostMapping("save")
public Result save(@RequestBody @Validated SysRole role) {
sysRoleService.save(role);
return Result.ok();
}
@PreAuthorize("hasAuthority('bnt.sysRole.update')")
@ApiOperation(value = "修改角色")
@PutMapping("update")
public Result updateById(@RequestBody SysRole role) {
sysRoleService.updateById(role);
return Result.ok();
}
@PreAuthorize("hasAuthority('bnt.sysRole.remove')")
@ApiOperation(value = "删除角色")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
sysRoleService.removeById(id);
return Result.ok();
}
@PreAuthorize("hasAuthority('bnt.sysRole.remove')")
@ApiOperation(value = "根据id列表删除")
@DeleteMapping("batchRemove")
public Result batchRemove(@RequestBody List<Long> idList) {
sysRoleService.removeByIds(idList);
return Result.ok();
}
...
}
测试服务器端权限
登录后台,分配权限进行测试,页面如果添加了按钮权限控制,可临时去除方便测试
测试结论:
1、分配了权限的能够成功返回接口数据
2、没有分配权限的会抛出异常:org.springframework.security.access.AccessDeniedException: 不允许访问
异常处理
异常处理有2种方式:
1、扩展Spring Security异常处理类:AccessDeniedHandler、AuthenticationEntryPoint
2、在spring boot全局异常统一处理
第一种方案说明:如果系统实现了全局异常处理,那么全局异常首先会获取AccessDeniedException异常,要想Spring Security扩展异常生效,必须在全局异常再次抛出该异常。
我们使用第二种方案。
全局异常添加处理
操作模块:service-util
/**
* spring security异常
* @param e
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public Result error(AccessDeniedException e) throws AccessDeniedException {
return Result.build(null, ResultCodeEnum.PERMISSION);
}
AccessDeniedException需要引入依赖,Spring Security对应的异常
在service-util模块引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>provided</scope>
</dependency>
浙公网安备 33010602011771号