废话
本案例是以最简单最简单的方式实现动态权限配置,摒弃各种花里胡哨的代码。
动态权限主要需要实现两个功能:
1、Url访问权限的动态设置
2、用户本身具备的权限动态设置
基础逻辑主要就是用Security作为登录、权限校验,权限允许则访问,权限不允许则提示权限不足。
一、准备工作
1、一个简单的SpringBoot工程
略
2、引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
3、数据源
正常需要三张数据表,用户表、权限表、Url表,然后你就需要读取数据库、连Mybatis、JDBC、Redis巴拉巴拉的,这个跟主题无关,此处用三个写死的类替代就好。
(1)、权限管理类,枚举出来本系统的全部权限
@Service
public class RoseService {
public final String ADMIN = "ADMIN";
public final String USER = "USER";
}
(2)、Url管理类,指定那些Url需要哪些权限才能访问
@Service
public class UrlService {
@Autowired
RoseService roseService;
public String[] getRose(String url) {
if (url.equals("user")) {
return new String[]{roseService.USER};
}
if (url.equals("admin")) {
return new String[]{roseService.ADMIN, roseService.USER};
}
return new String[]{};
}
}
(3)、用户对象,必须实现Security的UserDetails的6个方法。
@Component
public class UserEntity implements UserDetails {
private String username;
private String password;
private String[] roses;
/**
* 获取当前用户对象所具有的角色信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String rose : roses) {
authorities.add(new SimpleGrantedAuthority(rose));
}
return authorities;
}
/**
* 获取当前用户对象的密码
*/
@Override
public String getPassword() {
return password;
}
/**
* 获取当前用户对象的用户名
*/
@Override
public String getUsername() {
return username;
}
/**
* 当前账户是否未过期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 当前账户是否未锁定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 当前账户密码是否未过期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 当前账户是否可用
*/
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setRose(String[] roses) {
this.roses = roses;
}
}
(4)、用户管理类,必须实现Security的UserDatailsService的loadUserByUsername方法,用户登录的时候Security会自动调用该方法
@Service
public class UserService implements UserDetailsService {
/**
* UserDetailsService接口中的实现方法(用户登陆时自动调用)
*
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = getUser(username);
if (null == user) {
throw new UsernameNotFoundException("账户不存在!");
}
return user;
}
public UserEntity getUser(String username) {
UserEntity user = null;
if (username.equals("admin")) {
user = new UserEntity();
user.setUsername("admin");
user.setPassword(new BCryptPasswordEncoder().encode("123456"));
user.setRose(new String[]{"ADMIN"});
}
if (username.equals("user")) {
user = new UserEntity();
user.setUsername("user");
user.setPassword(new BCryptPasswordEncoder().encode("123456"));
user.setRose(new String[]{"USER"});
}
return null;
}
}
4、一个简单的Controller
设定是这样的,open-不需要权限,admin支持ADMIN权限,user支持ADMIN、USER权限。
@RestController
public class TestController {
@RequestMapping(value = "open")
public Object open() {
return "open";
}
@RequestMapping(value = "admin")
public Object admin() {
return "admin";
}
@RequestMapping(value = "user")
public Object user() {
return "user";
}
}
二、开搞
需要准备最少三个类。
1、WebSecurityFilterInvocationSecurityMetadataSource
用于根据Url获取到该Url需要什么样的权限才能访问
@Component
public class WebSecurityFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
UrlService urlService;
private static Map<String, String[]> roseMap;
/**
* 将所有的url的权限信息都拿出来,当数据有变化时,记得再更新一次
* <p>
* 在getAttributes()方法内不可直接使用Mapper,因为Security先于Spring加载,此时还没有注入,会报空指针异常
* 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次,依赖注入初始化后会自动执行该方法
*/
@PostConstruct
private List<String> getRoses() {
System.out.println("Url权限数据已加载");
roseMap = new HashMap<>();
roseMap.put("/admin", urlService.getRose("admin"));
roseMap.put("/user", urlService.getRose("user"));
return null;
}
/**
* 通过当前请求的URL获取所需要的权限信息
*
* @param o(FilterInvocation) 用于获取当前请求的url
* @return 当前请求url所需的角色
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String url = ((FilterInvocation) o).getRequestUrl();
System.out.println("当前请求的Url:" + url);
String[] roses = roseMap.get(url);
return SecurityConfig.createList(roses);
}
/**
* 返回所有定义好的权限资源
* Spring Security在启动时会校验相关配置是否正确,如果不需要校验,直接返回null
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
System.out.println("返回所有定义好的权限资源");
return null;
}
/**
* 是否支持校验
*/
@Override
public boolean supports(Class<?> aClass) {
System.out.println("是否支持校验");
return true;
}
}
2、WebSecurityAccessDecisionManger
用于权限校验,对比传过来的Url需要的权限和当前用户拥有的权限去判定当前用户对当前Url是否有权访问
@Component
public class WebSecurityAccessDecisionManger implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//如果authentication是UsernamePasswordAuthenticationToken实例,说明当前用户已登录。
if (!(authentication instanceof UsernamePasswordAuthenticationToken)) {
System.out.println("----------------> 请先登录!");
throw new AccessDeniedException("请先登录");
}
Collection<? extends GrantedAuthority> auths = authentication.getAuthorities();
System.out.println("Url需要的权限:" + collection.toString());
System.out.println("用户当前的权限:" + auths.toString());
for (ConfigAttribute configAttribute : collection) {
//循环校验,有权限满足就证明是可以访问的
for (GrantedAuthority authority : auths) {
if (configAttribute.getAttribute().equals(authority.getAuthority())) {
return;
}
}
}
System.out.println("----------------> 权限不足!");
throw new AccessDeniedException("权限不足!");
}
/**
* 是否支持校验
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
System.out.println("configAttribute是否支持校验");
return true;
}
/**
* 是否支持校验
*/
@Override
public boolean supports(Class<?> aClass) {
System.out.println("aClass是否支持校验");
return true;
}
}
3、WebSecurityConfig
最核心的Security配置类
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/500").permitAll()//不过滤
.antMatchers("/403").permitAll()//不过滤
.antMatchers("/404").permitAll()//不过滤
//其他不过滤的Url,略
.anyRequest() //任何其它请求
.authenticated() //都需要身份认证
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
//自定义权限校验
object.setSecurityMetadataSource(new WebSecurityFilterInvocationSecurityMetadataSource());
object.setAccessDecisionManager(new WebSecurityAccessDecisionManger());
return object;
}
})
.and()
// 开启表单登录,即登录页
.formLogin()
// 自定义登录页,未配置下启用默认登录页
// .loginPage("/login_page")
// 登录成功之后跳转的页面
.loginProcessingUrl("/index")
// 登录参数-用户名
.usernameParameter("username")
// 登录参数-密码
.passwordParameter("password")
// 登录成功回调函数-返回登陆成功的JSON信息
.successHandler((request, response, authentication) -> {
System.out.println("-----登录成功-----> ");
})
// 登录失败回调函数-返回登陆失败的JSON信息
.failureHandler((request, response, e) -> {
System.out.println("-----登录失败-----> ");
})
// 和登录相关的接口都不需要认证即可访问
.permitAll()
.and()
// 开启注销登录配置
.logout()
// 配置注销登录请求URL,默认为 "/logout"
.logoutUrl("/logout")
// 是否清除身份认证信息,默认为true,表示清除
.clearAuthentication(true)
// 是否使session失效,默认为true
.invalidateHttpSession(true)
// 删除指定的Cookie信息,可以传入多个key
.deleteCookies("JSESSIONID")
// 注销回调函数,可以处理数据清除工作
.addLogoutHandler((request, response, authentication) -> {
System.out.println("-----注销回调----->");
this.getCookieMsg(request);
})
// 注销成功回调函数
.logoutSuccessHandler((request, response, authentication) -> {
System.out.println("-----注销成功----->");
// 注销成功后重定向到登录页
response.sendRedirect("/login");
})
.and()
.csrf().disable();
// http.addFilterBefore(authorizationSecurityInterceptor, FilterSecurityInterceptor.class);
}
/**
* 自定义方法,获取Cookie信息
*/
private void getCookieMsg(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (null == cookies || cookies.length < 1) {
System.out.println("------> Cookie已全部清除!");
} else {
System.out.println("---Cookie---> ");
}
}
//解决静态资源被拦截的问题
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**");
web.ignoring().antMatchers("/img/**");
web.ignoring().antMatchers("/plugins/**");
}
/**
* 密码加密方式,必须指定一种
* 本例使用官方推荐的BCrypt强哈希函数,密钥迭代次数为2^strength,strength取值在4~31之间,默认为10
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(15);
}
/**
* 认证设置(配置用户信息:登录名、登录密码、所属角色)
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}