前端可以根据权限信息控制菜单和页面展示,操作按钮的显示。但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的。所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问。
在进入Controller方法前判断当前用户是否拥有访问权限,可以通过Filter加AOP的方式实现认证和授权。本次介绍的是成熟的框架:Spring Security。其他框架还有Shiro等。
Spring Security简介
Spring Security的重要核心功能功能是“认证”和“授权”,即用户认证(Authentication)和用户授权(Authorization)两部分:
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求提供用户名和密码,系统通过校验用户名和密码来完成认证过程。
(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,用的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
Spring Security的特点:
- 和Spring无缝整合
- 全面的权限控制
- 专门为Web开发而设计
- 重量级
Spring Boot出现后,其为Spring Security提供了自动配置方案,可以使用少量的配置来使用Spring Security。如果你的项目是基于Spring Boot的,使用Spring Security无疑是很棒的选择!
Spring Security实现权限
要对Web资源进行保护,最好的办法莫过于Filter
要对方法调用进行保护,最好的方法莫过于AOP
Spring Security进行认证和鉴权的时候就是利用一系列的Filter进行拦截的。
如图所示,一个请求要想访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分就是负责异常处理,橙色部分则是负责授权。经过一系列拦截最终访问到我们的API。
- FilterSecurityInterceptor:是一个方法级的过滤器,基本位于过滤链的最底部。
- ExceptionTranslationFilter:是一个异常过滤器,用来处理在认证授权过程中抛出的异常。
- UsernamePasswordAuthenticationFilter:对
/login
的POST请求做拦截,校验表单中用户名、密码。
这里我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter
负责登陆认证,FilterSecurityInterceptor
负责权限授权。
说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。
用户认证流程
自定义组件
根据认证流程,我们需要自定义以下组件:
- UserDetails
- loadUserByUsername
- passwordEncoder
1、登陆Filter,判断用户名和密码是否正确,生成token
2、认证解析token组件,判断请求头是否有token,如果有认证完成
3、在配置类配置相关认证类
代码实现
完整项目地址:Server | GitHub
依赖
创建一个spring-security
模块(module),可以放在项目的common
模块下
创建完成,导入相关的Maven依赖
pom.xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency> </dependencies>
|
工具类
ResponseUtil
用于写会数据给前端
Import
importimport 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;
|
utils.ResponseUtil.javapublic 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); } } }
|
JwtHelper
Import
importpackage com.swx.common.jwt;
import io.jsonwebtoken.*; import org.springframework.util.StringUtils;
import java.util.Date;
|
jwt.JwtHelper.javapublic class JwtHelper {
private static long tokenExpiration = 60 * 60 * 1000; private static String tokenSignKey = "sw-code";
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) { if (StringUtils.isEmpty(token)) return null; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims body = claimsJws.getBody(); String userId = body.get("userId").toString(); return Long.parseLong(userId); }
public static String getUsername(String token) { if (StringUtils.isEmpty(token)) return null; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims body = claimsJws.getBody(); return (String) body.get("username"); }
}
|
自定义UserDetail
继承UserDetail的User,其中sysUser
是项目数据库的实体类
Import
importimport com.swx.model.system.SysUser; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User;
import java.util.Collection;
|
custom.CustomUser.javapublic 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
importimport com.swx.common.utils.MD5; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component;
|
custom.CustomMd5PasswordEncoder.java@Component public 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())); } }
|
自定义UserDetailsService
该类的实现类会查询项目的数据库,根据用户名获取用户信息,包括密码等,用于匹配和授权。
注意要继承org.springframework.security.core.userdetails.UserDetailsService
Import
importimport org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component;
|
custom.UserDetailsService.java@Component public 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
importimport 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;
|
impl.UserDetailsServiceImpl.java@Service public 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<Permission> permissions = sysMenuService.queryUserAuthListByUserId(sysUser.getId()); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); permissions.forEach(permission -> { authorities.add(new SimpleGrantedAuthority(permission.getAuth().trim())); }); return new CustomUser(sysUser, authorities); } }
|
拦截器
TokenLoginFilter
获得输入的用户名和密码,封装成框架要求的对象,调用认证方法。认证成功则将权限信息存入Redis,并返回Token给前端。
该类继承UsernamePasswordAuthenticationFilter
,实现登陆的拦截校验。
Import
importimport 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;
|
filter.TokenLoginFilter.java
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private final RedisTemplate<String, String> redisTemplate; public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate<String, String> 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(); String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername()); redisTemplate.opsForValue().set( customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities())); HashMap<String, Object> 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); } }
|
TokenAuthenticationFilter
判断是否完成认证,将认证信息保存到Security上下文中
Import
importimport 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;
|
filter.TokenAuthenticationFilter.java
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final RedisTemplate<String, String> redisTemplate;
public TokenAuthenticationFilter(RedisTemplate<String, String> 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; } try { 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)); } } catch (JwtException e) { SecurityContextHolder.getContext().setAuthentication(null); R r = R.fail(ResultCode.TOKEN_INVALID); if (e instanceof UnsupportedJwtException) { r.setResultCode(ResultCode.TOKEN_UNSUPPORTED); } ResponseUtil.out(response, r); } }
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (!StringUtils.isEmpty(token)) { String username = JwtHelper.getUsername(token); if (!StringUtils.isEmpty(username)) { String authString = redisTemplate.opsForValue().get(username); if (!StringUtils.isEmpty(authString)) { List<Map> mapList = JSON.parseArray(authString, Map.class); System.out.println(mapList); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); mapList.forEach(map -> { String authority = (String) map.get("authority"); authorities.add(new SimpleGrantedAuthority(authority)); }); return new UsernamePasswordAuthenticationToken(username, null, authorities); } else { return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList()); } } } return null; } }
|
配置文件
创建一个Spring Security的配置文件,开启相关的注解
Import
importimport 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;
|
config.WebSecurityConfig.java@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService;
private final CustomMd5PasswordEncoder customMd5PasswordEncoder;
private final RedisTemplate<String, String> redisTemplate;
public WebSecurityConfig(UserDetailsService userDetailsService, CustomMd5PasswordEncoder customMd5PasswordEncoder, RedisTemplate<String, String> 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信息
<dependency> <groupId>com.swx</groupId> <artifactId>spring-security</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
|
在需要授权的接口上加入注解,就像这样
RoleController.java@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<SysRole> findAll() { return sysRoleService.list(); }
@ApiOperation("条件分页查询") @PreAuthorize("hasAuthority('system_role_list')") @GetMapping("{page}/{limit}") public IPage<SysRole> pageQueryRole(@PathVariable Long page, @PathVariable Long limit, SysRoleQueryVo sysRoleQueryVo) { CustomPage<SysRole> pageParam = new CustomPage<>(page, limit); LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>(); String roleName = sysRoleQueryVo.getRoleName(); if (!StringUtils.isEmpty(roleName)) { wrapper.like(SysRole::getRoleName, roleName); } IPage<SysRole> 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("添加失败"); } }
@ApiOperation("根据ID查询") @PreAuthorize("hasAuthority('system_role_list')") @GetMapping("{id}") public SysRole get(@PathVariable Long id) { return sysRoleService.getById(id); }
@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<Long> ids) { boolean delete = sysRoleService.removeByIds(ids); if (!delete) { throw new BizException("删除失败"); } }
}
|