欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring boot 整合 spring security4

程序员文章站 2022-04-19 22:31:59
...

我们在编写Web应用时,经常需要对页面做一些安全控制,比如:对于没有访问权限的用户需要转到登录表单页面。要实现访问控制的方法多种多样,可以通过Aop、拦截器实现,也可以通过框架实现(如:Apache Shiro、Spring Security)。

本文将具体介绍在Spring Boot中如何使用Spring Security进行安全控制。
整体框架: spring boot spring data jpa spring security

心得

在整理当前框架时,遇到了几个问题

<sec:authorize access="hasRole('ROLE_USER')">
     这里是角色ROLE_USER可以看到
</sec:authorize> 
<sec:authorize url="/admin">
   这里是具有 /admin 资源的用户可以看到
</sec:authorize>

当时官网是这样描述着两个标签的
此标记用于确定是否应评估其内容。在spring 3.0,它可以以两种方式使用 。第一种方法使用了网络的安全性表达,在指定access标签的属性。表达式求值将被委托给SecurityExpressionHandler<FilterInvocation>应用程序上下文中定义(你应该在你的基于Web的表达<http>空间配置,以确保该服务可用)。所以,例如,你可能有<sec:authorize access="hasRole('ROLE_USER')"></sec:authorize>这这种标签可以直接使用 .
但是对于 URL 来讲就没那么简单了.需要自定义DefaultWebInvocationPrivilegeEvaluator类. 下面我会给出详细设计代码,在这之前我想多说一句,当时扩展的时候我遇到了标签不起作用,百度 谷歌了好久,也没有解决问题.我在群里问人的时候,群里的回答也是让我大写的服...一个个的都不认字吗?
有人回答说用 shiro 吧....有人回答说,谁还用 JSP... 有人回答说,自定义标签吧...有人回答说,用 hasrole 标签吧... url 没用....我真是服了,,我求求你们,你们是怎么当上程序员的啊!!!!!当别人问你们问题的时候,,你们的回答也是大写的服!!!!!! 别人用 jsp 咋了,,跟当前问的问题有任何关系吗?所以啊有什么问题还是靠自己解决啊.. 于是就跟踪源代码DefaultWebInvocationPrivilegeEvaluator. java中有个securityInterceptor属性.这个属性就决定是用扩展自定义的类还是用 springsecurity 本身自己的类...最后发现是我这个地方没有注入进去..查询了官方 API, 原来发现 javaconfig 的方式在在

 public void configure(WebSecurity web) throws Exception {
       web.securityInterceptor(myFilterSecurityInterceptor);
       web.privilegeEvaluator(customWebInvocationPrivilegeEvaluator());
}

这样才能做到注入自己的扩展的FilterSecurityInterceptor,下面我会给出详细代码.
参考文档 http://docs.spring.io/spring-security/site/docs/4.2.2.BUILD-SNAPSHOT/reference/htmlsingle/
http://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/config/annotation/web/builders/WebSecurity.html
解决问题还是得靠自己. 多看文档,多跟踪源代码,多看 API. 下面开始进入正题.

表设计

springsecurity框架的表设计还是很简单的, user 用户表, role 角色表, resource 资源表.然后三者通过关系关联,我这里设计了5张表, user,role,resource,user_role,role_resource 其中user_role表是用户与角色之间的关系,多对多,role_resource 关系也是这样.

实体类

user.java
@Entity
@Table(name = "ad_operator_info")
public class User extends BaseEntity {
   /**
    * 主键
    */
   @Id
   @GeneratedValue(generator = "uuid")
   @GenericGenerator(name = "uuid", strategy = "uuid")
   @Column(name = "oper_id", length = 32)
   private String operId;
   /**
    * 用户名
    */
   @Column(name = "user_name")
   private String userName;
   /**
    * 密码
    */
   private String password;

   @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
   @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
   private Set<Role> roles;

   //省略 get... set..
}
role.java
@Entity
@Table(name = "ad_role")
public class Role extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
    private Set<OperatorInfo> users;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "ad_roles_resources", joinColumns = {@JoinColumn(name = "rid")}, inverseJoinColumns = {@JoinColumn(name = "eid")})
    private Set<Resource> resources;
   // 省略 get set
}
Resource.java
@Entity
@Table(name = "ad_web_resource")
public class WebResource extends BaseEntity {

    private static final long serialVersionUID = 7926081201477024763L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 主键

    private String name; // 资源名称

    private String url;

    @Column(name="remark",length=200)
    private String remark;//备注

    @Column(name="methodName",length=400)
    private String methodName;//资源所对应的方法名

    @Column(name="methodPath",length=1000)
    private String methodPath;//资源所对应的包路径

    private String sn;

    private String value; // 资源标识
   
   // 省略 get set  这里的属性可以根据自己的业务来.
}

实体类就此准备完毕. 下面加入 springsecurity 的 jar 包

  1. 下载 jar
<parent>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-parent</artifactId>  
        <version>1.4.1.RELEASE</version>  
    </parent>  
    <dependencies>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-web</artifactId>  
        </dependency>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-security</artifactId>  
        </dependency>  
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>4.2.1.RELEASE</version>
        </dependency>
</dependencies>  

2 .Spring Security配置
创建Spring Security的配置类 WebSecurityConfig,也是注入自己定义扩展FilterSecurityInterceptor的重要类 ,具体如下:

import com.pwkj.potevio.adp.auth.MyFilterSecurityInterceptor;
import com.pwkj.potevio.adp.auth.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
 * Created by PrimaryKey on 17/2/4.
 *
 * @EnableWebSecurity: 禁用Boot的默认Security配置,配合@Configuration启用自定义配置(需要扩展WebSecurityConfigurerAdapter)
 * @EnableGlobalMethodSecurity(prePostEnabled = true): 启用Security注解,例如最常用的@PreAuthorize
 * configure(HttpSecurity): Request层面的配置,对应XML Configuration中的<http>元素
 * configure(WebSecurity): Web层面的配置,一般用来配置无需安全检查的路径
 * configure(AuthenticationManagerBuilder): 身份验证配置,用于注入自定义身份验证Bean和密码校验规则
 */


@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

   @Bean
    @Primary
    public DefaultWebInvocationPrivilegeEvaluator customWebInvocationPrivilegeEvaluator() {
        return new DefaultWebInvocationPrivilegeEvaluator(myFilterSecurityInterceptor);
    }
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // javaconfig 配置是这样 set 进去的.
        web.securityInterceptor(myFilterSecurityInterceptor);
        web.privilegeEvaluator(customWebInvocationPrivilegeEvaluator());
        web.
                ignoring()
                .antMatchers("/assets/**", "/login", "/login/success", "/kaptcha/**", "/**/*.jsp");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/resources", "/login", "/kaptcha/**").permitAll()//访问:这些路径 无需登录认证权限
                .anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问
                //.antMatchers("/resources").hasAuthority("ADMIN") //登陆后之后拥有“ADMIN”权限才可以访问/hello方法,否则系统会出现“403”权限不足的提示
         .and()
                .formLogin()
                .loginPage("/")//指定登录页是”/”
                .permitAll()
                .successHandler(loginSuccessHandler()) //登录成功后可使用loginSuccessHandler()存储用户信息,可选。
         .and()
                .logout()
                .logoutUrl("/admin/logout")
                .logoutSuccessUrl("/") //退出登录后的默认网址是”/home”
                .permitAll()
                .invalidateHttpSession(true);
               // .and()
                //.rememberMe()//登录后记住用户,下次自动登录,数据库中必须存在名为persistent_logins的表
                //.tokenValiditySeconds(1209600);
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //指定密码加密所使用的加密器为passwordEncoder()
        //需要将密码加密后写入数据库
        auth.userDetailsService(myUserDetailService);//.passwordEncoder(bCryptPasswordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(4);
    }

    @Bean
    public LoginSuccessHandler loginSuccessHandler() {
        return new LoginSuccessHandler();
    }
}

编写LoginSuccessHandler.java 此类是在登陆成功之后做一些业务操作

package com.pwkj.potevio.adp.config;

import com.pwkj.potevio.adp.entity.OperatorInfo;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by PrimaryKey on 17/2/4.
 */
public class LoginSuccessHandler extends
        SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication) throws IOException,
            ServletException {
        //获得授权后可得到用户信息   可使用OperatorInfoService进行数据库操作
        OperatorInfo userDetails = (OperatorInfo) authentication.getPrincipal();
       /* Set<SysRole> roles = userDetails.getSysRoles();*/
        //输出登录提示信息
        System.out.println("管理员 " + userDetails.getName() + " 登录");

        System.out.println("IP :" + getIpAddress(request));

        super.onAuthenticationSuccess(request, response, authentication);
    }


    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

下面是自定义的过滤器,也是最重要的集成代码.
首先编写 MyInvocationSecurityMetadataSource.java 此类是首先加载的,用于加载资源配置.用resourceMap对象存储url --> value

package com.pwkj.potevio.adp.auth;

/**
 * Created by PrimaryKey on 17/2/4.
 */

import com.pwkj.potevio.adp.dao.WebResourceDao;
import com.pwkj.potevio.adp.entity.WebResource;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;

import javax.annotation.PostConstruct;
import java.util.*;

@Service
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
    private org.slf4j.Logger LOG = LoggerFactory.getLogger(getClass());

    @Autowired
    private WebResourceDao webResourceDao;

    /**
     * 加载资源,初始化资源变量
     */
    @PostConstruct
    public void loadResourceDefine() {
        if (resourceMap == null) {
            resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
            List<WebResource> resources = webResourceDao.findAll();
            for (WebResource resource : resources) {
                Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
                ConfigAttribute configAttribute = new SecurityConfig(resource.getValue());
                configAttributes.add(configAttribute);
                resourceMap.put(resource.getUrl(), configAttributes);
            }
        }
        LOG.info("security info load success!!");
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if (resourceMap == null) loadResourceDefine();
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
       // 返回当前 url  所需要的权限
         return resourceMap.get(requestUrl);
    }



    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

其次编写 MyUserDetailService.java 此类用来获取用户的所有权限.

package com.pwkj.potevio.adp.auth;

import com.pwkj.potevio.adp.entity.OperatorInfo;
import com.pwkj.potevio.adp.entity.Role;
import com.pwkj.potevio.adp.entity.WebResource;
import com.pwkj.potevio.adp.service.OperatorInfoService;
import com.pwkj.potevio.adp.service.WebResourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * Created by PrimaryKey on 17/2/4.
 * 二
 */
@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private OperatorInfoService operatorInfoService;

    @Autowired
    private WebResourceService webResourceService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //取得用户
        OperatorInfo operatorInfo = operatorInfoService.findByUserName(userName);
        if (operatorInfo == null) {
            throw new UsernameNotFoundException("UserName " + userName + " not found");
        }
        // 取得用户的权限
        Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(operatorInfo);
        Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
        for (Role role : operatorInfo.getRoles()) {
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        // 封装成spring security的user
        User userDetail = new User(operatorInfo.getUserName(), operatorInfo.getPassword(),
                true,//是否可用
                true,//是否过期
                true,//证书不过期为true
                true,//账户未锁定为true ,
                grantedAuths);
        return userDetail;
    }

    // 取得用户的权限
    private Set<GrantedAuthority> obtionGrantedAuthorities(OperatorInfo operatorInfo) {
        List<WebResource> resources = new ArrayList<WebResource>();
        //获取用户的角色
        Set<Role> roles = operatorInfo.getRoles();
        for (Role role : roles) {
            Set<WebResource> res = role.getResources();
            for (WebResource resource : res) {
                resources.add(resource);
            }
        }
        Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
        for (WebResource r : resources) {
            //用户可以访问的资源名称(或者说用户所拥有的权限)
            authSet.add(new SimpleGrantedAuthority(r.getValue()));
        }
        return authSet;
    }
}
```
再次编写 ```MyFilterSecurityInterceptor.java``` 用于跳转
```
package com.pwkj.potevio.adp.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;

import javax.servlet.*;
import java.io.IOException;

/**
 * Created by PrimaryKey on 17/2/4.
 *
 * 三
 */
@Service
public class MyFilterSecurityInterceptor extends FilterSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        invoke(fi);
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //fi里面有一个被拦截的url
        //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
        //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

```
最后编写```MyAccessDecisionManager.java``` 类用来判断当前用户是否有访问权限.
```
package com.pwkj.potevio.adp.auth;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.Iterator;

/**
 * Created by PrimaryKey on 17/2/4.
 *
 * 最后一个类
 */
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {


        // TODO  权限 .... >>>
        if (configAttributes == null) {
            return;
        }
        //所请求的资源拥有的权限(一个资源对多个权限)
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //访问所请求资源所需要的权限
            String needPermission = configAttribute.getAttribute();
             //用户所拥有的权限authentication
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                System.out.println("-----------PrimaryKey-----------ga.getAuthority()值=" + ga.getAuthority() + "," + "当前类=MyAccessDecisionManager.decide()");
                if (needPermission.equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("没有权限访问!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

```
 

到此java 代码就已经完成编写了. 然后抓紧时间写个```LoginController``` 吧

```
    @PostMapping("/login")
    public String login(String userName, String password,Model model) {
        HttpSession session = request.getSession();
        User user = userService.findByUserName(userName);
        if (!passwordEncoder.matches(password, user.getPassword())) {
             model.addAttribute("error", "用户名或密码错误");
            return "/pages/login";
        }
        // 这句代码会自动执行咱们自定义的 ```MyUserDetailService.java``` 类
        Authentication authentication = myAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
        if (!authentication.isAuthenticated()) {
            throw new BadCredentialsException("Unknown username or password");
        }
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);
        session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
        session.setAttribute(PlatformConstant.SESSION_OPERATOR, user);
         operateLogService.saveOperateLog(user, request.getRemoteAddr());
        return "index";
    }
```
页面如下
```login.jsp```
```
<form action="/user/login"method="POST">
<table>
    <tr>
        <td>username:</td>
        <td><input type='text'name='username'></td>
    </tr>
    <tr>
        <td>password:</td>
        <td><input type='password'name='password'></td>
    </tr>
    <tr>
        <td><input name="reset"type="reset"></td>
        <td><input name="submit"type="submit"></td>
    </tr>
</table>
</form>
```
登陆之后跳转到 index.jsp 
```
这是首页,欢迎<sec:authentication property="name"/>!<br>

<sec:authentication property="authorities"/>  <br/>

<a href="admin.jsp">进入admin页面</a>

<sec:authorize url='/other1.jsp' >
    <a href="other1.jsp">权限1</a>
</sec:authorize> 

<sec:authorize url='/other2.jsp' >
<a href="other2.jsp">权限2</a>
</sec:authorize>

<sec:authorize url='/other3.jsp' >
    <a href="other3.jsp">权限3</a>
</sec:authorize> 
```
到此,整个标签库都会生效了,,,由于时间有限,,写的有点仓促了,哪里不懂的可以问我.小伙伴抓紧时间试试吧...