前端可以根据权限信息控制菜单和页面展示,操作按钮的显示。但这并不够,如果有人拿到了接口,绕过了页面直接操作数据,这是很危险的。所以我们需要在后端也加入权限控制,只有拥有操作权限,该接口才能被授权访问。
在进入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
import| 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;
 
 | 
 utils.ResponseUtil.java| 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);
 }
 }
 }
 
 
 | 
 
JwtHelper
Import
import| package com.swx.common.jwt;
 import io.jsonwebtoken.*;
 import org.springframework.util.StringUtils;
 
 import java.util.Date;
 
 | 
 jwt.JwtHelper.java| public 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
import| import com.swx.model.system.SysUser;import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 
 import java.util.Collection;
 
 | 
 custom.CustomUser.java| 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
import| import com.swx.common.utils.MD5;import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Component;
 
 | 
 custom.CustomMd5PasswordEncoder.java| @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()));
 }
 }
 
 | 
 
自定义UserDetailsService
该类的实现类会查询项目的数据库,根据用户名获取用户信息,包括密码等,用于匹配和授权。
注意要继承org.springframework.security.core.userdetails.UserDetailsService
Import
import| import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Component;
 
 | 
 custom.UserDetailsService.java| @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
import| 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;
 
 | 
 impl.UserDetailsServiceImpl.java| @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<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
import| 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;
 
 | 
 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
import| 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;
 
 | 
 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
import| 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;
 
 | 
 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("删除失败");
 }
 }
 
 }
 
 |