博客园2023-04-11 12:10:55
前端可以根据权限信息控制菜单和页面展示,操作按钮的显示。但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的。所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问。
在进入Controller方法前判断当前用户是否拥有访问权限,可以通过Filter加AOP的方式实现认证和授权。本次介绍的是成熟的框架:Spring Security。其他框架还有Shiro等。
Spring Security的重要核心功能功能是“认证”和“授权”,即用户认证(Authentication)和用户授权(Authorization)两部分:
(资料图片仅供参考)
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求提供用户名和密码,系统通过校验用户名和密码来完成认证过程。
(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,用的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
Spring Security的特点:
Spring Boot出现后,其为Spring Security提供了自动配置方案,可以使用少量的配置来使用Spring Security。如果你的项目是基于Spring Boot的,使用Spring Security无疑是很棒的选择!
要对Web资源进行保护,最好的办法莫过于Filter
要对方法调用进行保护,最好的方法莫过于AOP
Spring Security进行认证和鉴权的时候就是利用一系列的Filter进行拦截的。
如图所示,一个请求要想访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分就是负责异常处理,橙色部分则是负责授权。经过一系列拦截最终访问到我们的API。
/login的POST请求做拦截,校验表单中用户名、密码。这里我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter负责登陆认证,FilterSecurityInterceptor负责权限授权。
说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。
根据认证流程,我们需要自定义以下组件:
1、登陆Filter,判断用户名和密码是否正确,生成token
2、认证解析token组件,判断请求头是否有token,如果有认证完成
3、在配置类配置相关认证类
完整项目地址:Server | GitHub
创建一个spring-security模块(module),可以放在项目的common模块下
创建完成,导入相关的Maven依赖
org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web provided 用于写会数据给前端
import com.fasterxml.jackson.databind.ObjectMapper;import com.swx.common.pojo.R;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class ResponseUtil { public static void out(HttpServletResponse response, R r) { ObjectMapper mapper = new ObjectMapper(); response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); try { mapper.writeValue(response.getWriter(), r); } catch (IOException e) { throw new RuntimeException(e); } }}package com.swx.common.jwt;import io.jsonwebtoken.*;import org.springframework.util.StringUtils;import java.util.Date;public class JwtHelper { private static long tokenExpiration = 60 * 60 * 1000; private static String tokenSignKey = "xxxxxx"; public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject("AUTH-USER") .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) .claim("userId", userId) .claim("username", username) .signWith(SignatureAlgorithm.HS512, tokenSignKey) .compressWith(CompressionCodecs.GZIP) .compact(); } public static Long getUserId(String token) { try { if (StringUtils.isEmpty(token)) return null; Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims body = claimsJws.getBody(); String userId = body.get("userId").toString(); return Long.parseLong(userId); } catch (Exception e) { e.printStackTrace(); return null; } } public static String getUsername(String token) { try { if (StringUtils.isEmpty(token)) return null; Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims body = claimsJws.getBody(); return (String) body.get("username"); } catch (Exception e) { e.printStackTrace(); return null; } }} 继承UserDetail的User,其中sysUser是项目数据库的实体类
import com.swx.model.system.SysUser;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.User;import java.util.Collection;public class CustomUser extends User { private SysUser sysUser; public CustomUser(SysUser sysUser, Collection extends GrantedAuthority> authorities) { super(sysUser.getUsername(), sysUser.getPassword(), authorities); this.sysUser = sysUser; } public SysUser getSysUser() { return sysUser; } public void setSysUser(SysUser sysUser) { this.sysUser = sysUser; }}用于匹配前端传过来的密码和数据库中的密码是否一致,其中MD5.encrypt是自定义的MD5加密工具
MD5:MD5 | GitHub
import com.swx.common.utils.MD5;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Component;@Componentpublic class CustomMd5PasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); }}该类的实现类会查询项目的数据库,根据用户名获取用户信息,包括密码等,用于匹配和授权。
注意要继承
org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Component;@Componentpublic interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService { /** * 根据用户名获取用户对象,获取不到直接抛异常 */ @Override UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}实现该类
该实现可以放到项目的
service.impl中,就像项目其他Service的实现类一样SysUserService:SysUserServiceImpl | GitHub
SysMenuService:SysMenuServiceImpl | GitHub
Permission:Permission | GitHub
import com.swx.auth.service.SysMenuService;import com.swx.auth.service.SysUserService;import com.swx.model.system.SysUser;import com.swx.security.custom.CustomUser;import com.swx.security.custom.UserDetailsService;import com.swx.vo.system.Permission;import org.springframework.security.authentication.DisabledException;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.Collections;import java.util.List;@Servicepublic class UserDetailsServiceImpl implements UserDetailsService { private final SysUserService sysUserService; private final SysMenuService sysMenuService; public UserDetailsServiceImpl(SysUserService sysUserService, SysMenuService sysMenuService) { this.sysUserService = sysUserService; this.sysMenuService = sysMenuService; } /** * 根据用户名获取用户对象,获取不到直接抛异常 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名查询 SysUser sysUser = sysUserService.getUserByUsername(username); if (null == sysUser) { throw new UsernameNotFoundException("用户名不存在!"); } if (sysUser.getStatus() == 0) { throw new DisabledException("disable"); } // 查询权限列表 List permissions = sysMenuService.queryUserAuthListByUserId(sysUser.getId()); // 封装Spring Security的权限类型 List authorities = new ArrayList<>(); permissions.forEach(permission -> { authorities.add(new SimpleGrantedAuthority(permission.getAuth().trim())); }); return new CustomUser(sysUser, authorities); }} 获得输入的用户名和密码,封装成框架要求的对象,调用认证方法。认证成功则将权限信息存入Redis,并返回Token给前端。
该类继承UsernamePasswordAuthenticationFilter,实现登陆的拦截校验。
import com.alibaba.fastjson2.JSON;import com.fasterxml.jackson.databind.ObjectMapper;import com.swx.common.jwt.JwtHelper;import com.swx.common.pojo.R;import com.swx.common.pojo.ResultCode;import com.swx.common.utils.ResponseUtil;import com.swx.security.custom.CustomUser;import com.swx.vo.system.LoginVo;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.authentication.DisabledException;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.HashMap;/** * 获得输入的用户名和密码,封装成框架要求的对象,调用认证方法 */public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private final RedisTemplate redisTemplate; // 构造方法 public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; this.setAuthenticationManager(authenticationManager); this.setPostOnly(false); // 指定登陆接口及提交方式,可以指定任意路径 this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login", "POST")); } // 登陆认证 // 获取输入的用户名和密码,调用方法认证 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { // 获取用户信息 LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class); // 封装对象 UsernamePasswordAuthenticationToken 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(); // 生成Token String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername()); // 获取当前用户的权限数据,放到Redis中,key: username value: permissions redisTemplate.opsForValue().set( customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities())); // 返回 HashMap map = new HashMap<>(); map.put("token", token); ResponseUtil.out(response, R.success(map)); } // 认证失败调用的方法 @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { // 封装错误信息,用于返回 R r = R.fail(ResultCode.LOGIN_AUTH_FAIL); Throwable ex = failed.getCause(); if (ex instanceof DisabledException) { r.setResultCode(ResultCode.USER_DISABLE); } else if (failed instanceof UsernameNotFoundException || failed instanceof BadCredentialsException) { r.setResultCode(ResultCode.USER_LOGIN_ERROR); } ResponseUtil.out(response, r); }} 判断是否完成认证,将认证信息保存到Security上下文中
import com.alibaba.fastjson2.JSON;import com.swx.common.jwt.JwtHelper;import com.swx.common.pojo.R;import com.swx.common.pojo.ResultCode;import com.swx.common.utils.ResponseUtil;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.util.StringUtils;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;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Map;/** * 判断是否完成认证 */public class TokenAuthenticationFilter extends OncePerRequestFilter { private final RedisTemplate redisTemplate; public TokenAuthenticationFilter(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 如果是登陆接口,直接放行 if ("/admin/system/index/login".equals(request.getRequestURI())) { chain.doFilter(request, response); return; } UsernamePasswordAuthenticationToken authentication = getAuthentication(request); if (null != authentication) { SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } else { ResponseUtil.out(response, R.fail(ResultCode.LOGIN_AUTH_FAIL)); } } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (!StringUtils.isEmpty(token)) { String username = JwtHelper.getUsername(token); if (!StringUtils.isEmpty(username)) { // 从redis中获取权限数据 String authString = redisTemplate.opsForValue().get(username); if (!StringUtils.isEmpty(authString)) { List 创建一个Spring Security的配置文件,开启相关的注解
import com.swx.security.custom.CustomMd5PasswordEncoder;import com.swx.security.filter.TokenAuthenticationFilter;import com.swx.security.filter.TokenLoginFilter;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService; private final CustomMd5PasswordEncoder customMd5PasswordEncoder; private final RedisTemplate redisTemplate; public WebSecurityConfig(UserDetailsService userDetailsService, CustomMd5PasswordEncoder customMd5PasswordEncoder, RedisTemplate redisTemplate) { this.userDetailsService = userDetailsService; this.customMd5PasswordEncoder = customMd5PasswordEncoder; this.redisTemplate = redisTemplate; } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .cors().and() .authorizeRequests() .antMatchers("/admin/system/index/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class) .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate)); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/favicon.icon", "/swagger-resources/**", "webjars/**", "/v2/**", "swagger-ui.html/**", "doc.html"); }} 可以在业务模块中导入pom信息
com.swx spring-security 1.0-SNAPSHOT 在需要授权的接口上加入注解,就像这样
@Api(tags = "角色管理接口")@RestController@ResponseResult@RequestMapping("/admin/system/sysRole")public class SysRoleController { private final SysRoleService sysRoleService; public SysRoleController(SysRoleService sysRoleService) { this.sysRoleService = sysRoleService; } @ApiOperation("为用户分配角色") @PreAuthorize("hasAuthority("system_role_assign")") @PostMapping("/doAssign") public void doAssign(@RequestBody AssignRoleVo assignRoleVo) { sysRoleService.doAssign(assignRoleVo); } @ApiOperation("查询所有角色") @PreAuthorize("hasAuthority("system_role_list")") @GetMapping("/findAll") public List findAll() { return sysRoleService.list(); } /** * * @param page 当前页 * @param limit 记录数 * @param sysRoleQueryVo 查询参数 * @return 分页信息 */ @ApiOperation("条件分页查询") @PreAuthorize("hasAuthority("system_role_list")") @GetMapping("{page}/{limit}") public IPage pageQueryRole(@PathVariable Long page, @PathVariable Long limit, SysRoleQueryVo sysRoleQueryVo) { // 自定义Page,修改current为page,和前端保持一致 CustomPage pageParam = new CustomPage<>(page, limit); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); String roleName = sysRoleQueryVo.getRoleName(); if (!StringUtils.isEmpty(roleName)) { wrapper.like(SysRole::getRoleName, roleName); } IPage iPage = sysRoleService.page(pageParam, wrapper); return iPage; } @ApiOperation("添加角色") @PreAuthorize("hasAuthority("system_role_add")") @PostMapping("") public void save(@RequestBody SysRole role) { boolean save = sysRoleService.save(role); if (!save) { throw new BizException("添加失败"); } } /** * 根据id查询角色 * @param id 角色id * @return 角色 */ @ApiOperation("根据ID查询") @PreAuthorize("hasAuthority("system_role_list")") @GetMapping("{id}") public SysRole get(@PathVariable Long id) { return sysRoleService.getById(id); } /** * 更新角色 * @param role 角色信息 */ @ApiOperation("更新角色") @PreAuthorize("hasAuthority("system_role_update")") @PutMapping("") public void update(@RequestBody SysRole role) { boolean update = sysRoleService.updateById(role); if (!update) { throw new BizException("更新失败"); } } @ApiOperation("根据id删除") @PreAuthorize("hasAuthority("system_role_remove")") @DeleteMapping("{id}") public void delete(@PathVariable Long id) { boolean delete = sysRoleService.removeById(id); if (!delete) { throw new BizException("删除失败"); } } @ApiOperation("批量删除") @PreAuthorize("hasAuthority("system_role_remove")") @DeleteMapping("batch") public void batchRemove(@RequestBody List ids) { boolean delete = sysRoleService.removeByIds(ids); if (!delete) { throw new BizException("删除失败"); } }} 关键词: