• spring security入门demo


    一、前言

      因项目需要引入spring security权限框架,而之前也没接触过这个一门,于是就花了点时间弄了个小demo出来,说实话,刚开始接触这个确实有点懵,看网上资料写的权限大都是静态,即就是在配置文件或代码里面写定角色,不能动态更改,个人感觉这样实际场景应该应用的不多,于是就进一步研究,整理出了一个可以动态管理个人权限角色demo,其中可能有很多不足或之处,还望指正。本文通过spring boot集成spring security,处理方式没有使用xml文件格式,而是用了注解。

     二、表结构

    接触过权限这块的,大都应该知道,最核心的有三张表(当然,如果牵涉业务复杂,可能不止)。

    一、用户表

    二、角色表

    三、菜单表(即权限表)

    剩余还有两张多对多的表。即用户与角色,角色与菜单。如下图

    三、spring security入口

    由于本文只是着重说spring security,关于spring boot一块内容会直接带过。如spring boot启动类配置等。

    首先会自定义一个类去实现WebSecurityConfigurerAdapter类。重写其中几个方法,代码如下

     1 @Configuration
     2 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     3 
     4     @Autowired
     5     @Qualifier(value = "userDetailServiceImpl")
     6     private UserDetailsService userDetailsService;
     7 
     8     @Autowired
     9     private LoginSuccessAuthenticationHandler successAuthenticationHandler;
    10 
    11     @Autowired
    12     private LoginFailureAuthenticationHandler failureAuthenticationHandler;
    13 
    14     @Autowired
    15     private AuthenticationAccessDeniedHandler accessDeniedHandler;
    16 
    17     @Autowired
    18     private UrlAccessDecisionManager decisionManager;
    19 
    20     @Autowired
    21     private UrlPathFilterInvocationSecurityMetadataSource urlPathFilterInvocationSecurityMetadataSource;
    22 
    23     @Autowired
    24     private AuthenticationProvider authenticationProvider;
    25 
    26     @Autowired
    27     private PasswordEncoder passwordEncoder;
    28 
    29     @Override
    30     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    31         auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    32         auth.authenticationProvider(authenticationProvider);
    33     }
    34 
    35     @Override
    36     public void configure(WebSecurity web) {
    37         web.ignoring().antMatchers("/index.html","/favicon.ico");
    38     }
    39 
    40     @Override
    41     protected void configure(HttpSecurity http) throws Exception {
    42         http.csrf().disable()
    43                 .authorizeRequests()
    44                 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    45                     @Override
    46                     public <O extends FilterSecurityInterceptor> O postProcess(O o) {
    47                         o.setAccessDecisionManager(decisionManager);
    48                         o.setSecurityMetadataSource(urlPathFilterInvocationSecurityMetadataSource);
    49                         return o;
    50                     }
    51                 })
    52 
    53                 .anyRequest()
    54                 .authenticated()// 其他 url 需要身份认证
    55 
    56                 .and()
    57                 .formLogin()  //开启登录,如果不指定登录路径(即输入用户名和密码表单提交的路径),则会默认为spring securtiy的内部定义的路径
    58                 .successHandler(successAuthenticationHandler)
    59                 .failureHandler(failureAuthenticationHandler)// 遇到用户名或密码不正确/用户被锁定等情况异常,会交给此handler处理
    60                 .permitAll()
    61 
    62                 .and()
    63                 .logout()
    64                 .logoutUrl("/logout")//退出操作,其实也有一个handler,如果没其他业务逻辑,可以默认为spring security的handler
    65                 .permitAll()
    66                 .and()
    67                 .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
    68     }

    在这里会介绍以下几个类作用

    一、UserDetailsService
    二、AuthenticationProvider
    三、AuthenticationAccessDeniedHandler
    四、UrlAccessDecisionManager
    五、UrlPathFilterInvocationSecurityMetadataSource
    至于LoginSuccessAuthenticationHandler、LoginFailureAuthenticationHandler就是用来处理登录成功和登录失败情况,这里不做介绍

    3.1、UserDetailService的作用

    这个一个接口,通常我们需要去实现它,作用主要是用来我们和数据库做交互用的。简单来说,就是用户名传过来,这个类负责校验用户名是否存在等业务逻辑。

     1 @Component
     2 public class UserDetailServiceImpl implements UserDetailsService {
     3 
     4     @Autowired
     5     private SysUserDAO userDAO;
     6 
     7     @Autowired
     8     private PasswordEncoder passwordEncoder;
     9 
    10 
    11     @Override
    12     public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    13         SysUser sysUser = userDAO.findByUsername(s);
    14         if (sysUser == null){
    15             throw new UsernameNotFoundException("用户不存在");
    16         }
    17         String pwd = passwordEncoder.encode(sysUser.getPassword());
    18         System.out.println(pwd);
    19         return new User(sysUser.getUsername(),pwd,getRoles(sysUser.getRoles()));
    20     }
    21 
    22     private Collection<GrantedAuthority> getRoles(List<SysRole> roles){
    23         List<GrantedAuthority> list = new ArrayList<>();
    24         for (SysRole role : roles){
    25             SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
    26             list.add(grantedAuthority);
    27         }
    28         return list;
    29     }
    30 }

    代码比较简单,值得注意的是sercurity里的User对象,它的一个构造函数有是哪个参数值,第一个和第二个是用户名和密码,密码作用就是后面用来校验前端传过来的密码正确性。稍后会讲到。至于第三个参数就是当前用户所拥有的角色,作用就是在当前端请求一个接口的时候,会判断这个接口所拥有的权限和该用户所有的权限有重合,简单来说就是该用户是否拥有该接口权限。这里也就实现了一个角色可以动态修改的功能。因其实从数据库查询出来。

    3.2、AuthenticationProvider

    它也是一个接口,它的作用是用来校验用户密码等功能,当然如短信验证或要第三方验证,也可以实现这个接口,在本文中是用密码校验。前面也说到userDetailService会传一个用户的基本信息。它的主要作用就是为该接口服务的。

     1 @Component
     2 public class LoginAuthenticationProvider implements AuthenticationProvider {
     3 
     4     @Autowired
     5     private UserDetailsService userDetailsService;
     6 
     7     @Override
     8     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
     9         // 获取表单用户名
    10         String username = (String) authentication.getPrincipal();
    11         // 获取表单用户填写的密码
    12         String password = (String) authentication.getCredentials();
    13 
    14         UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    15 
    16         String password1 = userDetails.getPassword();
    17         if (!Objects.equals(password,password1)){
    18             throw new BadCredentialsException("用户名或密码不正确");
    19         }
    20 
    21         return new UsernamePasswordAuthenticationToken(username,password,userDetails.getAuthorities());
    22     }
    23 
    24     @Override
    25     public boolean supports(Class<?> aClass) {
    26         return true;
    27     }
    28 }

    值得注意的是如果验证通过会返回一个UsernamePasswordAuthenticationToken对象,它的作用就是标志着此用户已通过登录验证,如果没通过,则spring security会捕捉如代码18行的异常,然后再包装一个匿名的token,即AnonymousAuthenticationToken,此token即代表用户未登录。两个接口主要服务于用户登录这块。接下来的三个是服务于权限校验。即接口验证

    3.3、UrlPathFilterInvocationSecurityMetadataSource

     它的作用是用来处理当前用户是否拥有此接口的权限。

     1 @Component
     2 public class UrlPathFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
     3 
     4 
     5     @Autowired
     6     private SysMenuDAO sysMenuDAO;
     7 
     8     private AntPathMatcher antPathMatcher = new AntPathMatcher();
     9 
    10     @Override
    11     public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    12         FilterInvocation filterInvocation = (FilterInvocation) object;
    13         String requestUrl = filterInvocation.getRequestUrl();
    14         // 因为菜单一般随着开发完成,变动不大,此处可以使用缓存,这里为了演示,就直接查库,菜单对应角色需要动态情缓存,如变更菜单和角色关系,需清除缓存
    15         List<SysMenu> all = sysMenuDAO.findAll();
    16         for (SysMenu menu : all) {
    17             if (menu.getRoles().size() != 0 && antPathMatcher.match(menu.getUrlPath(), requestUrl)) {
    18                 List<SysRole> roles = menu.getRoles();
    19                 int size = roles.size();
    20                 String[] values = new String[size];
    21                 for (int i = 0; i < size; i++) {
    22                     values[i] = roles.get(i).getRoleName();
    23                 }
    24                 return SecurityConfig.createList(values);
    25             }
    26         }
    27         return SecurityConfig.createList("ROLE_LOGIN");
    28     }
    29 
    30     @Override
    31     public Collection<ConfigAttribute> getAllConfigAttributes() {
    32         return null;
    33     }
    34 
    35     @Override
    36     public boolean supports(Class<?> clazz) {
    37         return true;
    38     }
    39 }

    从代码就可以看出16行的for循环就是获取当前请求接口锁需要的权限,这里使用spring security的路径匹配类。如果该接口·没有权限,这里返回一个标志如ROLE_LOGIN,当然如果需要其他标志可以自行定义,这里为了简便,就用了这个。

    3.4、UrlAccessDecisionManager

    这个类就是最终的决策类。从3.1到3.2,大家都清楚,已有的信息,用户所有的权限这个已经获取到了,3.3可知当前请求接口的权限也已经获取到了,剩下的肯定就是比较两这个权限集合有没有交集,如果有则表明当前用户拥有此接口的权限。

     1 @Component
     2 public class UrlAccessDecisionManager implements AccessDecisionManager {
     3 
     4     /**
     5      *
     6      * @param authentication 当前用户信息,和当前用户的拥有权限信息,即来自于userDetailService里的
     7      * @param object 即FilterInvocation对象,可以获取httpServletRequest请求对象
     8      * @param configAttributes  本次访问所需要的权限
     9      * @throws AccessDeniedException
    10      * @throws InsufficientAuthenticationException
    11      */
    12     @Override
    13     public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
    14         Iterator<ConfigAttribute> iterator = configAttributes.iterator();
    15         while (iterator.hasNext()) {
    16             ConfigAttribute ca = iterator.next();
    17             //当前请求需要的权限
    18             String needRole = ca.getAttribute();
    19             if ("ROLE_LOGIN".equals(needRole)) {
    20                 // 即匿名用户/未登录,如果用户登录成功。那么authententication就是前面提到的UsernamePasswordAuthententicationToken类
    21                 if (authentication instanceof AnonymousAuthenticationToken) {
    22                     throw new BadCredentialsException("未登录");
    23                 } else {// 登录但不具有此路径权限,即前面3.3提到的ROLE_LOGIN,接口没有角色对应,主要用户已经登录成功
    24                     break;
    25                 }
    26             }
    27             //当前用户所具有的权限
    28             Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    29             for (GrantedAuthority authority : authorities) {
    30                 if (authority.getAuthority().equals(needRole)) {
    31                     return;
    32                 }
    33             }
    34         }
    35         throw new AccessDeniedException("权限不足!");
    36     }
    37 
    38     @Override
    39     public boolean supports(ConfigAttribute attribute) {
    40         return true;
    41     }
    42 
    43     @Override
    44     public boolean supports(Class<?> clazz) {
    45         return true;
    46     }
    47 }

    3.5、AuthenticationAccessDeniedHandler

    这个类就是用来接收上面抛出的accessDeniedException异常,

     1 @Component
     2 public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
     3 
     4 
     5     @Override
     6     public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
     7         httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
     8         httpServletResponse.setContentType("application/json;charset=UTF-8");
     9         PrintWriter writer = httpServletResponse.getWriter();
    10 
    11         writer.print("权限不足");
    12         writer.flush();
    13     }
    14 }

    至于哪种异常由哪个类处理,如果了解源码的都知道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 内部视图解析器配置,如下面

    1 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    2   <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    3   <property name="prefix" value="/WEB-INF/" />
    4   <property name="suffix" value=".jsp" /> 
    5    <!-- 重点是下面配置,将其改为false -->
    6   <property name="redirectHttp10Compatible" value="false" />
    7 </bean>

    不过redirect也提醒了我,这个情况由https 变成http 应该就是redirect搞的鬼。那如果将spring security内部由redirect改成forward呢,那情况又会怎样,紧接着,又去看其源码,最后发现这样一个类LoginUrlAuthenticationEntryPoint负责spring security的重定向和转发情况,在其commence方法内进行操作,最后那肯定得试试,最后将该类的useForward属性设置成了true,然后就完美解决。

     --------------------------------------------------------------------------------------------------------------------------------------------------分界线--------------------------------------------------------------------------------------

    以上就是全部内容,若有不足之处,还望指正,另外附上本文代码地址供大家参考 spring security demo

  • 相关阅读:
    2020蓝桥杯模拟赛(一)
    自己整理的瀑布流+滚动加载图片的例子
    .NET如何发送格式化的文本内容
    Bootstrap学习笔记(3)--表格表单图片
    BootStap学习笔记(2)
    BootStap学习笔记(1)
    Oracle性能优化
    Maven+spring+springMVC+mybatis+Junit+Log4j配置个人总结
    C#指针和寻址运算
    Linq to XML
  • 原文地址:https://www.cnblogs.com/qm-article/p/10388166.html
Copyright © 2020-2023  润新知