一, 前言
因项目需要引入 spring security 权限框架, 而之前也没接触过这个一门, 于是就花了点时间弄了个小 demo 出来, 说实话, 刚开始接触这个确实有点懵, 看网上资料写的权限大都是静态, 即就是在配置文件或代码里面写定角色, 不能动态更改, 个人感觉这样实际场景应该应用的不多, 于是就进一步研究, 整理出了一个可以动态管理个人权限角色 demo, 其中可能有很多不足或之处, 还望指正. 本文通过 spring boot 集成 spring security, 处理方式没有使用 xml 文件格式, 而是用了注解.
二, 表结构
接触过权限这块的, 大都应该知道, 最核心的有三张表 (当然, 如果牵涉业务复杂, 可能不止).
一, 用户表
二, 角色表
三, 菜单表 (即权限表)
剩余还有两张多对多的表. 即用户与角色, 角色与菜单. 如下图
三, spring security 入口
由于本文只是着重说 spring security, 关于 spring boot 一块内容会直接带过. 如 spring boot 启动类配置等.
首先会自定义一个类去实现 webSecurityConfigurerAdapter 类. 重写其中几个方法, 代码如下
- @Configuration
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
- @Autowired
- @Qualifier(value = "userDetailServiceImpl")
- private UserDetailsService userDetailsService;
- @Autowired
- private LoginSuccessAuthenticationHandler successAuthenticationHandler;
- @Autowired
- private LoginFailureAuthenticationHandler failureAuthenticationHandler;
- @Autowired
- private AuthenticationAccessDeniedHandler accessDeniedHandler;
- @Autowired
- private UrlAccessDecisionManager decisionManager;
- @Autowired
- private UrlPathFilterInvocationSecurityMetadataSource urlPathFilterInvocationSecurityMetadataSource;
- @Autowired
- private AuthenticationProvider authenticationProvider;
- @Autowired
- private PasswordEncoder passwordEncoder;
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
- auth.authenticationProvider(authenticationProvider);
- }
- @Override
- public void configure(WebSecurity Web) {
- Web.ignoring().antMatchers("/index.html","/favicon.ico");
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.csrf().disable()
- .authorizeRequests()
- .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
- @Override
- public <O extends FilterSecurityInterceptor> O postProcess(O o) {
- o.setAccessDecisionManager(decisionManager);
- o.setSecurityMetadataSource(urlPathFilterInvocationSecurityMetadataSource);
- return o;
- }
- })
- .anyRequest()
- .authenticated()// 其他 url 需要身份认证
- .and()
- .formLogin() // 开启登录, 如果不指定登录路径 (即输入用户名和密码表单提交的路径), 则会默认为 spring securtiy 的内部定义的路径
- .successHandler(successAuthenticationHandler)
- .failureHandler(failureAuthenticationHandler)// 遇到用户名或密码不正确 / 用户被锁定等情况异常, 会交给此 handler 处理
- .permitAll()
- .and()
- .logout()
- .logoutUrl("/logout")// 退出操作, 其实也有一个 handler, 如果没其他业务逻辑, 可以默认为 spring security 的 handler
- .permitAll()
- .and()
- .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
- }
在这里会介绍以下几个类作用
一, UserDetailsService
二, AuthenticationProvider
三, AuthenticationAccessDeniedHandler
四, UrlAccessDecisionManager
五, UrlPathFilterInvocationSecurityMetadataSource
至于 LoginSuccessAuthenticationHandler,LoginFailureAuthenticationHandler 就是用来处理登录成功和登录失败情况, 这里不做介绍
3.1,UserDetailService 的作用
这个一个接口, 通常我们需要去实现它, 作用主要是用来我们和数据库做交互用的. 简单来说, 就是用户名传过来, 这个类负责校验用户名是否存在等业务逻辑.
- @Component
- public class UserDetailServiceImpl implements UserDetailsService {
- @Autowired
- private SysUserDAO userDAO;
- @Autowired
- private PasswordEncoder passwordEncoder;
- @Override
- public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
- SysUser sysUser = userDAO.findByUsername(s);
- if (sysUser == null){
- throw new UsernameNotFoundException("用户不存在");
- }
- String pwd = passwordEncoder.encode(sysUser.getPassword());
- System.out.println(pwd);
- return new User(sysUser.getUsername(),pwd,getRoles(sysUser.getRoles()));
- }
- private Collection<GrantedAuthority> getRoles(List<SysRole> roles){
- List<GrantedAuthority> list = new ArrayList<>();
- for (SysRole role : roles){
- SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
- list.add(grantedAuthority);
- }
- return list;
- }
- }
代码比较简单, 值得注意的是 sercurity 里的 User 对象, 它的一个构造函数有是哪个参数值, 第一个和第二个是用户名和密码, 密码作用就是后面用来校验前端传过来的密码正确性. 稍后会讲到. 至于第三个参数就是当前用户所拥有的角色, 作用就是在当前端请求一个接口的时候, 会判断这个接口所拥有的权限和该用户所有的权限有重合, 简单来说就是该用户是否拥有该接口权限. 这里也就实现了一个角色可以动态修改的功能. 因其实从数据库查询出来.
3.2,AuthenticationProvider
它也是一个接口, 它的作用是用来校验用户密码等功能, 当然如短信验证或要第三方验证, 也可以实现这个接口, 在本文中是用密码校验. 前面也说到 userDetailService 会传一个用户的基本信息. 它的主要作用就是为该接口服务的.
- @Component
- public class LoginAuthenticationProvider implements AuthenticationProvider {
- @Autowired
- private UserDetailsService userDetailsService;
- @Override
- public Authentication authenticate(Authentication authentication) throws AuthenticationException {
- // 获取表单用户名
- String username = (String) authentication.getPrincipal();
- // 获取表单用户填写的密码
- String password = (String) authentication.getCredentials();
- UserDetails userDetails = userDetailsService.loadUserByUsername(username);
- String password1 = userDetails.getPassword();
- if (!Objects.equals(password,password1)){
- throw new BadCredentialsException("用户名或密码不正确");
- }
- return new UsernamePasswordAuthenticationToken(username,password,userDetails.getAuthorities());
- }
- @Override
- public boolean supports(Class<?> aClass) {
- return true;
- }
- }
值得注意的是如果验证通过会返回一个 UsernamePasswordAuthenticationToken 对象, 它的作用就是标志着此用户已通过登录验证, 如果没通过, 则 spring security 会捕捉如代码 18 行的异常, 然后再包装一个匿名的 token, 即 AnonymousAuthenticationToken, 此 token 即代表用户未登录. 两个接口主要服务于用户登录这块. 接下来的三个是服务于权限校验. 即接口验证
3.3,UrlPathFilterInvocationSecurityMetadataSource
它的作用是用来处理当前用户是否拥有此接口的权限.
- @Component
- public class UrlPathFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
- @Autowired
- private SysMenuDAO sysMenuDAO;
- private AntPathMatcher antPathMatcher = new AntPathMatcher();
- @Override
- public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
- FilterInvocation filterInvocation = (FilterInvocation) object;
- String requestUrl = filterInvocation.getRequestUrl();
- // 因为菜单一般随着开发完成, 变动不大, 此处可以使用缓存, 这里为了演示, 就直接查库, 菜单对应角色需要动态情缓存, 如变更菜单和角色关系, 需清除缓存
- List<SysMenu> all = sysMenuDAO.findAll();
- for (SysMenu menu : all) {
- if (menu.getRoles().size() != 0 && antPathMatcher.match(menu.getUrlPath(), requestUrl)) {
- List<SysRole> roles = menu.getRoles();
- int size = roles.size();
- String[] values = new String[size];
- for (int i = 0; i <size; i++) {
- values[i] = roles.get(i).getRoleName();
- }
- return SecurityConfig.createList(values);
- }
- }
- return SecurityConfig.createList("ROLE_LOGIN");
- }
- @Override
- public Collection<ConfigAttribute> getAllConfigAttributes() {
- return null;
- }
- @Override
- public boolean supports(Class<?> clazz) {
- return true;
- }
- }
从代码就可以看出 16 行的 for 循环就是获取当前请求接口锁需要的权限, 这里使用 spring security 的路径匹配类. 如果该接口. 没有权限, 这里返回一个标志如 ROLE_LOGIN, 当然如果需要其他标志可以自行定义, 这里为了简便, 就用了这个.
3.4,UrlAccessDecisionManager
这个类就是最终的决策类. 从 3.1 到 3.2, 大家都清楚, 已有的信息, 用户所有的权限这个已经获取到了, 3.3 可知当前请求接口的权限也已经获取到了, 剩下的肯定就是比较两这个权限集合有没有交集, 如果有则表明当前用户拥有此接口的权限.
- @Component
- public class UrlAccessDecisionManager implements AccessDecisionManager {
- /**
- *
- * @param authentication 当前用户信息, 和当前用户的拥有权限信息, 即来自于 userDetailService 里的
- * @param object 即 FilterInvocation 对象, 可以获取 httpServletRequest 请求对象
- * @param configAttributes 本次访问所需要的权限
- * @throws AccessDeniedException
- * @throws InsufficientAuthenticationException
- */
- @Override
- public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
- Iterator<ConfigAttribute> iterator = configAttributes.iterator();
- while (iterator.hasNext()) {
- ConfigAttribute ca = iterator.next();
- // 当前请求需要的权限
- String needRole = ca.getAttribute();
- if ("ROLE_LOGIN".equals(needRole)) {
- // 即匿名用户 / 未登录, 如果用户登录成功. 那么 authententication 就是前面提到的 UsernamePasswordAuthententicationToken 类
- if (authentication instanceof AnonymousAuthenticationToken) {
- throw new BadCredentialsException("未登录");
- } else {// 登录但不具有此路径权限, 即前面 3.3 提到的 ROLE_LOGIN, 接口没有角色对应, 主要用户已经登录成功
- break;
- }
- }
- // 当前用户所具有的权限
- 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;
- }
- }
- 3.5,AuthenticationAccessDeniedHandler
这个类就是用来接收上面抛出的 accessDeniedException 异常,
- @Component
- public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
- @Override
- public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
- httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
- httpServletResponse.setContentType("application/json;charset=UTF-8");
- PrintWriter writer = httpServletResponse.getWriter();
- writer.print("权限不足");
- writer.flush();
- }
- }
至于哪种异常由哪个类处理, 如果了解源码的都知道 spring security 有一个异常处理过滤器, 名字为 ExceptionTranslationFilter, 要想进一步了解的, 可自行看源码, 这里提供一个个人认为写的挺好的博文, 链接地址, 这里不多说废话.
相信大家看完以上文章, 对 spring security 应该有一个大致的了解,, 这里附上一个 spring security 请求经过的过滤器 Filter,
执行顺序从上到下. 要想研究一波, 大家可以先从 DelegatingFilterProxy 类及它的父类开始入手, 一步一步 debug 下去, 相信会有收获的. 关于 WebSecurityConfig 的配置情况, 这里也不多说, 网上文章也挺多的. 在这里说下当初遇到的一个比较坑的坑
四, 遇到的坑
当时场景是这样的, 因为项目采用的是前后端分离模式开发的, 后端写完代码需要部署到测试服务器, 供前端使用, 采用的域名是 https 模式, 使用了 nginx 代码模式, 部署上去后. 因为登录失败后, spring security 会请求到你指定的一个路径, 但此时问题出现了, 代码部署上去了, 测试了一个用户名和密码不正确的情况, 结果发现跳转后的 host 由 https 变成了 http, 例子: 本来是请求 https://abc.com/doLogin 路径, 但是变成了 htttp://abc.com/doLogin. 这肯定是访问不了, 当时就有点懵了, 后面经过分析发现, 更改 Nginx 配置可以达到指定效果, 在指定的 location 加入 proxy_set_header X-Forwarded-Proto https, 但是这样局限性也有, 这样做只能使用 https 进行访问, 所以就没采用, 后来就直接百度, 百度了的结果大都是更改 spring mvc 内部视图解析器配置, 如下面
- <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
- <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
- <property name="prefix" value="/WEB-INF/" />
- <property name="suffix" value=".jsp" />
- <!-- 重点是下面配置, 将其改为 false -->
- <property name="redirectHttp10Compatible" value="false" />
- </bean>
不过 redirect 也提醒了我, 这个情况由 https 变成 http 应该就是 redirect 搞的鬼. 那如果将 spring security 内部由 redirect 改成 forward 呢, 那情况又会怎样, 紧接着, 又去看其源码, 最后发现这样一个类 LoginUrlAuthenticationEntryPoint 负责 spring security 的重定向和转发情况, 在其 commence 方法内进行操作, 最后那肯定得试试, 最后将该类的 useForward 属性设置成了 true, 然后就完美解决.
-------------------------------------------------------------------------------------------------------------------------------------------------- 分界线 --------------------------------------------------------------------------------------
以上就是全部内容, 若有不足之处, 还望指正, 另外附上本文代码地址供大家参考 spring security demo
来源: https://www.cnblogs.com/qm-article/p/10388166.html