本文的目的是如何基于 Spring Security 去扩展实现一个基本的用户权限模块, 内容会覆盖到 Spring Security 常用的配置.
文中涉及到的业务代码是不完善的, 甚至会存在逻辑上的漏洞, 业务部分请自行思考完善.
Spring Security、Shiro 这种所谓的安全框架, 其核心作用就是 认证 和 鉴权, 当不使用这些框架时, 比较常规的实现方式如下:
下面贴出的是核心代码(有删减)
// 登录接口
class LoginController {
@PostMapping("/login")
public Object login(HttpServletRequest request, HttpServletResponse response) {
// 获取登录用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 忽略非空校验
// 根据用户名查询数据库
User user = userDao.selectByUsername(username);
if(user == null) {
return "用户名不存在, 返回登录页";
}
// 验证密码
if(!password.equals(user.getPassword)) {
return "密码不正确, 返回登录页";
}
return "登录成功";
}
}
// 权限过滤器
class PermissionFilter implements Filter {
public void doFilter(HttpServletRequest request, HttpServletResponse response) {
// 如果是登录请求, 直接放行
String url = request.getRequestUrl();
if("/login".equals(url)) {
return "放行";
}
// 从 session 中取出用户信息(包含用户名、权限等关键信息)
User user = session.get('user');
// 如果 user == null, 表示未登录
if user == null {
return "跳转至登录页";
}
// 判断用户是否有当前请求的访问权限
if(user.getPermissions().contains(url)) {
return "放行";
}
return "没有访问权限!"
}
}
以上便是一个基本的、固定思维的 认证和 **鉴权 **的思路, 它的缺点是 可移植性 和 扩展性不高. 改造难度类似于给一台苹果笔记本换内存条一样.
Spring Security 在扩展性上给我们带来的体验就像给一台普通台式机换内存条, 基本上就是买个内存条, 然后一插一拔就完事了.
它把第一种实现方式中的涉及到的可变因素, 都提供出了对应的配置项, 因此, 实现认证和鉴权就跟组装变形金刚一样.
下面来分析下, 如果要对上述的 伪代码进行配置抽取, 至少都需要配置什么:
登录接口
url
, 以及获取表单数据的参数名称, 例如: username
、password
redis
、mysql
、memory
等)中取出用户信息(涉及到从哪里取, 怎么取).权限过滤器
/user
必须具有 admin
角色的用户才能访问(涉及到如何访问规则, 以及如何匹配这些规则.)下面看一下 Spring Security 是如何配置上面提到的内容的:
首先需要了解的是 Spring Security 是有自己的默认配置的
http://localhost:port/login
访问username
和 password
user
和 密码 (项目启动时, 会在控制台打印), 存放于内存中.前面说到, Spring Security 有自己的默认配置, 如果要自定义这些配置, 我们就要通过某种方式, 去覆盖重写原有的配置, Spring Security 为我们提供了相应的入口 :
WebSecurityConfig
配置类, 添加 @EnableWebSecurity
注解WebSecurityConfigurerAdapter
类// 第一步: 添加注解
@EnableWebSecurity
// 第二步: 继承配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// TODO 第三步: 重写方法
}
上一步, 我们已经找到了重写配置的切入点, 下面就接着上一步, 去真正自定义配置
username
和 password
, 如果不需要, 可以不配置loginPage("/login.html") 指定页面路径
/login
, 可以通过 loginProcessingUrl()
指定csrf
, 这里不多解释该名词, 自行了解.@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 如果需要自定义登录页, 需要增加如下配置, 不让静态资源走框架过滤器
// 当然也有其它方式可以做到, 我们采用这种推荐方式
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login.html");
// 如果需要排除其他静态资源, 参考如下:
//web.ignoring().antMatchers("/login.html", "/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置表单登录
http.formLogin()
// 第一步: 配置如何接收表单参数
.usernameParameter("username")
.passwordParameter("password")
// 第二步: 配置登录页路径, 这里指定的是 /resource/static/login.html
.loginPage("/login.html")
// 第三步: 指定表单提交的 url
.loginProcessingUrl("/login");
// 第四步: 关闭 csrf
http.csrf().disable();
}
}
在第二步中, 相当于配置了前后端交互的规则, 而接下来就要考虑后端的具体处理逻辑. 当后端接收到页面表单提交的参数后, 需要做什么:
下面来看一下 Spring Security 是如何处理上述的需求的:
UserDetailsService 的 ``loadUserByUsername(username)
方法获取 **用户信息, **然后该方法会返回一个 UserDetails
对象,loadUserByUsername
方法, 我们需要把查询出来的用户信息 封装到UserDetails
中.**于是有了如下步骤: **
UserDetails
用户类MyUserDetailsService
MyUserDetailsService
通过配置的方式注入到 Spring Security 中// 第一步:userDetail 包含了用户的详细信息, 需要按照 Spring Security 提供的接口实现
public class MyUserDetails implements UserDetails {
// 省略部分代码
}
// 第二步: 查询用户信息
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 省略查询用户信息(角色、权限等)
// 封装到 MyUserDetails 中
return new MyUserDetails(xxxxxxxxxxx);
}
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 第三步: 配置 UserDeatilsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new MyUserDetailsService());
}
// 如果需要自定义登录页, 需要增加如下配置, 不让静态资源走框架过滤器
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login.html");
// 如果需要排除其他静态资源, 参考如下:
//web.ignoring().antMatchers("/login.html", "/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭跨站攻击
http.csrf().disable();
// 配置表单登录
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.loginPage("/login.html");
}
}
至此, 其实已经完成了用户认证的过程, 我们主要做了以下事情:
UserDetails
对象中UserDetails
验证用户身份的过程认证过后, 就是访问权限的控制, 即: 根据当前请求的 url
, 判断用户是否具有访问权限
当访问一个 url
时, 你需要告诉 Spring Security, 访问这个 url
需要什么条件:
比如: /user
必须是 admin
角色才能访问, 亦或者是 admin
用户, 这个可以自定义实现.
下面的例子就以角色作为条件:
获取访问 **/user**
路径时, 需要的角色
/user
) 可以被多个角色访问**/user**
** **可以被 admin
和 superadmin
角色访问admin
和 superadmin
)@Component
public class MyFilterInvocationSecurityMetadataSource implements
FilterInvocationSecurityMetadataSource {
@Autowired
private MenuMapper menuMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws
IllegalArgumentException {
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 获取请求的 url
String requestUrl = ((FilterInvocation) o).getRequestUrl();
// 获取所有菜单以及对应的角色
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
// 如果查询到需要的条件, 就返回
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
List<Role> 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);
}
}
// 如果匹配不到,就表示该 url 不需要任何权限, Anonymous 是自己定义的
return SecurityConfig.createList("Anonymous");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
在上一步中, 我们拿到了访问一个 url 需要哪些角色, 下面就需要告诉 Spring Security 如何基于这些条件做出抉择( **放行 or 拒绝通行 **)
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o,
Collection<ConfigAttribute> collection) throws AccessDeniedException,
AuthenticationException {
// 这里获取到访问当前 url 需要的条件
Iterator<ConfigAttribute> iterator = collection.iterator();
//
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
// 获取
String needRole = ca.getAttribute();
// 如果可以匿名访问, 就放行
if ("Anonymous".equals(needRole)) {
return;
}
//当前用户所具有的权限
Collection<? extends GrantedAuthority> authorities =
authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
将第一步 和 第二步 的配置告诉 Spring Security
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilterInvocationSecurityMetadataSource
myFilterInvocationSecurityMetadataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login.html", "/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭跨站攻击
http.csrf().disable();
// 配置表单登录
// 配置表单登录
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.loginPage("/login.html");
// 配置动态权限
http.authorizeRequests()
.withObjectPostProcessor(
new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
o.setAccessDecisionManager(myAccessDecisionManager);
return o;
}
});
// 关闭 csrf
http.csrf().disable();
}
}
前面提到, 如果用户未登录, Spring Security 会跳转至登录页, 如果鉴权失败, 会跳转至 403 forbidden 页面. 这些也是可以通过重写对应的方法去实现的, 详情请参考:
关注点请放在 Spring Security 配置部分, 业务部分请自行完善, 当然, 后续自己也会基于此代码搭建一个完整的用户权限系统.
https://github.com/nimo10050/spring-security-sample/tree/master/spring-security-06