背景
前后端分离项目,直接暴露api接口,是很危险的一件事情,因此需要引入安全管理框架。在安全管理这个领域,我们熟知的框架有Shiro、Spring Security,考虑到后端使用Spring Boot,并且它Spring Security提供了自动化配置方案,可以零配置使用,因此选择Spring Security。
当然Shiro与SSM(Spring+SpringMVC+MyBatis)更搭配。
文章篇幅较长,建议在PC上浏览,且其左侧有【目录】,便于快速预览大纲以及定位!
一、安全管理SpringSecurity
1、概述
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,致力于为Java应用程序提供身份验证和授权。具有如下特点:
1. 全面和可扩展的身份验证和授权支持
2. 防御会话固定,点击劫持,跨站点请求伪造等攻击
3. Servlet API集成
4. 与Spring Web MVC的可选集成
2、执行路程
通过流程图,可以认为SpringSecurity是一组filter过滤器组成的权限认证。
流程图说明:
WebAsyncManagerIntegrationFilter:
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
SecurityContextPersistenceFilter:
在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
HeaderWriterFilter:用于将头信息加入响应中。
CsrfFilter:用于处理跨站请求伪造。
LogoutFilter:用于处理退出登录。
UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
BasicAuthenticationFilter:检测和处理 http basic 认证。
RequestCacheAwareFilter:用来处理请求的缓存。
SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
SessionManagementFilter:管理 session 的过滤器
ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
FilterSecurityInterceptor:可以看做过滤器链的出口。
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
3、注解
这里讲解下方法级安全的几个注解,@PreAuthorize, @PostAuthorize, @Secured。
3.1 开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
}
3.2 @Secured
@GetMapping("/hello")
@Secured({"ROLE_normal","ROLE_admin"})
public String hello() {
return "hello world";
}
说明:拥有normal或者admin角色的用户都可以访问hello方法,但若要求同时拥有则,则@Secured无能为力。
3.3 @PreAuthorize
@GetMapping("/hello")
@PreAuthorize("hasAnyRole('normal','admin')")
public String hello() {
return "hello world";
}
3.4 @PostAuthorize
在方法执行后再进行权限校验,适合验证带有返回值的权限。
@GetMapping("/hello")
@PostAuthorize(" returnObject!=null && returnObject.username == authentication.name")
public User hello() {
Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user;
if("anonymousUser".equals(pricipal)) {
user = null;
}else {
user = (User) pricipal;
}
return user;
}
4、自定义配置
Spring Boot为Spring Security提供的可零配置的自动化配置方案本文不做介绍。
4.1 添加依赖
环境:采用Spring Initializr快速构建的Spring Boot。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
4.2 创建SpringSecurity自定义配置类
自定义配置
EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/profile/**").anonymous()
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
认证失败处理类,返回未授权
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException
{
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
登出处理类,返回成功
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Autowired
private TokenService tokenService;
* 退出处理
*
* @return
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
String userName = loginUser.getUsername();
tokenService.delLoginUser(loginUser.getToken());
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
}
}
5、自定义请求权限校验
5.1 自定义权限服务
@Service("ss")
public class PermissionService{
public boolean hasPermi(String permission){
if (StringUtils.isEmpty(permission)){
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
....
}
public class ServletUtils{
public static HttpServletRequest getRequest(){
return getRequestAttributes().getRequest();
}
public static ServletRequestAttributes getRequestAttributes(){
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
...
}
5.2 请求添加注解
@Log(title = "角色管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:role:export')")
@GetMapping("/export")
public AjaxResult export(SysRole role)
{
List<SysRole> list = roleService.selectRoleList(role);
ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
return util.exportExcel(list, "角色数据");
}
5.3 自定义用户认证逻辑
通过SpringSecurity集成JWT的方式来实现,4.2节配置文件已声明,下面一起看看具体实现方式。
二、用户身份验证JWT
1、概述
JWT是JSON Web Token的简称,是一个开放标准(RFC 7519),用于作为JSON对象在各方之间安全的传输信息。该信息可以被验证和信任,属于数字签名,存在于客户端。
JWT广泛应用于Authorization(授权)和Informatica Exchange(信息交换)。一方面是因为它开销小,并且可以轻松跨域使用;另一方面是JWT可以被签名,如使用公钥/秘钥对,确定发送方,并且签名是使用头和有效负载计算,还可以验证内容有无被篡改。
2、JWT结构
JWT是由Header、Payload、Signature三部分组成,直接用圆点(".")连接。典型的JWT看起来像下面这个样式:
其中Header和Payload可以通过base64解密出来,因此通常不能在Payload中放置敏感的信息。
Header的构成:由token类型和算法名称组成
{
'alg': "HS256",
'typ': "JWT"
}
Payload的构成:包含声明,通常是关于实体(用户)和其他数据的声明,base64对这个JSON编码就得到了Payload。
{
user_key:'ada_xbasf_weet',
time:'12394395'
}
Signature的构成:需由经过编码的header、payload和一个秘钥,经过header指定的签名算法进行签名而生成。例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于消息传递中有无被篡改,并且使用私钥签名的token,还可以验证JWT的发送方。
3、 JWT认证流程
4、JWT生成
用户登录成功后,我们这里将用户信息写入redies,并使用uuid随机数作为key,配置过期时间等数据。
同时使用uuid,作为JWT的声明,创建token返回前端,代码仅供流程参考:
@Component
public class TokenService{
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put("user_key", token);
return createToken(claims);
}
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
...
}
5、集成JWT
我们在SpringSecurity中已经添加了Jwt过滤器,用于验证token的有效性,下面看看实现:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
至此我们简单梳理实现了,登录令牌(token)的生成,以及SpringSecurity集成JWT对token的验证。
三、前端认证权限控制
1、axios封装请求携带token
import axios from 'axios'
import store from '@/store'
import { getToken } from '@/utils/auth'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 30 * 1000
})
service.interceptors.request.use(config => {
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
...
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
2、页面模块权限控制
页面模块根据用户权限来展示,这里可以使用Vue的指令来实现。
页面UI声明权限
<template>
<div v-hasPermi="['system:user:admin']">
<button>管理员权限才可以点击</button>
</div>
</template>
创建全局指令
import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding
const all_permission = '*:*:*'
const permissions = store.getters && store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
const hasPermissions = permissions.some(permission => {
return all_permission === permission || permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}
import hasPermis from './hasPermis.js'
const install = function(Vue) {
Vue.directive('hasPermi', hasPermi)
}
if (window.Vue) {
window['hasPermi'] = hasPermi
Vue.use(install);
}
export default install
3、Vue指令讲解
Vue指令原理:
常用于对Dom的底层进行操作。
指令本质上是装饰器,是vue对HTML元素的扩展,给HTML元素增加自定义功能,语义化HTML标签。
vue编译DOM时,会执行与指令关联的JS代码,即找到指令对象,执行指令对象的相关方法。
Vue指令钩子函数:
Vue.directive('my-directive', {
bind: function () {},
inserted: function () {},
update: function () {},
componentUpdated: function () {},
unbind: function () {}
})
四、总结
至此本文梳理了前后端安全管理实现,涉及SpringSecurity、JWT以及axios封装请求、Vue指令,能够较完备的满足前后端分离开发的用户身份认证、UI权限控制、接口权限控制等需求。当然权限通常是根据角色挂靠,关于角色的控制,与权限同理,便不再叙述。
评论