1. 写在前面
权限设计无非就是:用户-角色-菜单,再加上两张中间表。
首先需要给角色赋予权限菜单,然后再把角色赋给相应的用户。比如人事部门主管用户名是hrUser,他的角色是hrRole,角色用有的权限是/hr/add、/hr/edit等,hrUser这个用户就可以操作人事相关的新增和修改操作。
在Spring Security要怎么实现呢,废话不多说,直接上代码。
2. 给用户添加角色信息
前面的文章我们已经实现了登录、获取菜单的功能,下面一段代码是为用户赋予角色。
2.1 User 实体类
package com.javaboy.vms.entity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 用户信息(VUser)实体类
*
* @author gaoyang
* @since 2021-04-20 14:26:27
*/
@Getter
@Setter
public class VUser implements Serializable, UserDetails {
private static final long serialVersionUID = -60957006911784869L;
/**
* 主键
*/
private Integer id;
/**
* 姓名
*/
private String name;
/**
* 手机号码
*/
private String phone;
/**
* 住宅电话
*/
private String telephone;
/**
* 联系地址
*/
private String address;
/**
* 是否启用
*/
private Boolean enabled;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 头像
*/
private String userface;
/**
* 备注
*/
private String remark;
/**
* 用户角色
*/
private List<VRole> roles;
/**
* 为用户赋予角色
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
for (VRole role:roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
2.2 User 服务实现类
此处在加载用户对象时,根据 userId 查询用户的所有角色并set给用户对象。
@Service("vUserService")
public class VUserServiceImpl implements VUserService, UserDetailsService {
@Resource
private VUserMapper vUserMapper;
/**
* 根据用户名加载用户对象
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
VUser user = this.vUserMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
user.setRoles(this.vUserMapper.getUserRoleById(user.getId()));
return user;
}
}
2.3 sql
根据用户id查询角色信息
<select id="getUserRoleById" resultType="com.javaboy.vms.entity.VRole">
select * from v_role r,v_user_role ur
where r.id = ur.role_id and ur.user_id = #{id}
</select>
好了,代码到这里就已经把角色赋给用户对象了。此时登录成功以后就可以看到相应的信息。
接下来我们来看具体的配置信息。
3. 权限校验
3.1 根据请求地址获取角色
用户登录以后,每发送一次请求,我们都要根据请求地址来获取这个地址所需要的角色信息,然后才可以判断该用户是否具备当前角色的权限。那么,我们直接看代码。
package com.javaboy.vms.config;
import com.javaboy.vms.entity.VMenu;
import com.javaboy.vms.entity.VRole;
import com.javaboy.vms.service.VMenuService;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
/**
* @author: gaoyang
* @date: 2021-05-26 15:10
* @description: 根据用户传来的请求地址,分析出请求需要的角色
*/
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Resource
private VMenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取当前请求地址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 获取所有的权限菜单
List<VMenu> menus = this.menuService.getAllMenusWithRole();
for (VMenu menu : menus) {
// 比较当前请求地址和权限菜单地址
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
// 如果url匹配则获取当前请求所需要的角色
List<VRole> roles = menu.getRoles();
String[] str = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();
}
return SecurityConfig.createList(str);
}
}
// 没有匹配,登录之后可以访问,返回标记信息
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* 获取该SecurityMetadataSource对象中保存的针对所有安全对象的权限信息的集合。
* 该方法的主要目的是被AbstractSecurityInterceptor用于启动时校验每个ConfigAttribute对象。
* @return
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 这里clazz表示安全对象的类型,该方法用于告知调用者当前SecurityMetadataSource是否支持此类安全对象,
* 只有支持的时候,才能对这类安全对象调用getAttributes方法
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
实现 FilterInvocationSecurityMetadataSource 接口重写 Collection 方法:
- object里有我们当前用户对象的基本信息,.getRequestUrl() 获取当前请求地址。
- 然后编写 this.menuService.getAllMenusWithRole() 方法获取所有的权限菜单
<select id="getAllMenusWithRole" resultMap="MenuWithRole">
select
m.*,r.id as rid,r.name as rname,r.name_zh as rname_zh
from
v_menu m,v_menu_role mr,v_role r
where
m.id = mr.menu_id and mr.role_id = r.id
order by
m.id
</select>
- 最后比较当前请求地址和权限菜单地址,获取当前地址所需要的角色,没有角色则返回标记信息”ROLE_LOGIN”。
3.2 判断当前用户是否具备角色
上面已经获取了当前请求路径的角色,接下来就要判断当前用户是否具备角色了。
package com.javaboy.vms.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @author: gaoyang
* @date: 2021-05-26 15:31
* @description: 判断当前用户是否具备角色
*/
@Slf4j
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
/**
*
* @param authentication 包含了当前的用户信息,包括拥有的权限。
* 这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
* @param object FilterInvocation对象,可以得到request等web资源。
* @param configAttributes 本次访问需要的权限。
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute:configAttributes){
// 需要的角色
String needRole = configAttribute.getAttribute();
if("ROLE_LOGIN".equals(needRole)){
// AnonymousAuthenticationToken 匿名认证
if (authentication instanceof AnonymousAuthenticationToken){
log.error("尚未登录,请登录!");
throw new AccessDeniedException("尚未登录,请登录!");
}else {
return;
}
}
// 获取当前登录用户的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority:authorities) {
// 判断是否具备当前登录的角色,如果有需要的角色或者是系统管理员则返回 authority.getAuthority().equals("ROLE_admin")
if (authority.getAuthority().equals(needRole)){
return;
}
}
}
log.error("权限不足,请联系管理员!");
throw new AccessDeniedException("权限不足,请联系管理员!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
代码注释很全,就不多做解释了。
4. 修改 Spring Security 配置类
首先,在 SecurityConfig 配置类注入刚才的两个bean:
@Resource
private CustomUrlDecisionManager customUrlDecisionManager;
@Resource
private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
然后修改 configure(HttpSecurity http) 方法:
http.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
完整代码
package com.javaboy.vms.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.javaboy.vms.entity.VUser;
import com.javaboy.vms.service.impl.VUserServiceImpl;
import com.javaboy.vms.util.ResultDTO;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author: gaoyang
* @date: 2021-04-15 16:35
* @description: Spring Security 配置类
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private VUserServiceImpl vUserService;
@Resource
private CustomUrlDecisionManager customUrlDecisionManager;
@Resource
private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(vUserService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/doLogin")
.usernameParameter("username")
.passwordParameter("password")
// 登录成功回调
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
VUser vUser = (VUser) authentication.getPrincipal();
vUser.setPassword(null);
ResultDTO resultDTO = ResultDTO.success("登录成功", vUser);
String s = new ObjectMapper().writeValueAsString(resultDTO);
out.write(s);
out.flush();
out.close();
}
})
// 登录失败回调
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
ResultDTO resultDTO = ResultDTO.error("登录失败");
if (exception instanceof LockedException) {
resultDTO.setMsg("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
resultDTO.setMsg("密码过期,请联系管理员!");
} else if (exception instanceof AccountExpiredException) {
resultDTO.setMsg("账户过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
resultDTO.setMsg("账户被禁用,请联系管理员!");
} else if (exception instanceof BadCredentialsException) {
resultDTO.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(resultDTO));
out.flush();
out.close();
}
})
.permitAll()
.and()
.logout()
// 登出回调
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(ResultDTO.success("注销成功!")));
out.flush();
out.close();
}
})
.permitAll()
.and()
.csrf().disable()
// 没有认证时,在这里处理结果,不要重定向
.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
ResultDTO resultDTO = ResultDTO.error("访问失败");
if (e instanceof InsufficientAuthenticationException) {
resultDTO.setMsg("请求失败,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(resultDTO));
out.flush();
out.close();
}
});
}
}
接下来我们测试一下:
- 登录 libai 用户
- 测试菜单接口
代码已传码云,有需自取:https://gitee.com/king-high/vms-master
技术交流+WX:JavaBoy_1024
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/5421.html