我们在编写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 包
- 下载 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>
```
到此,整个标签库都会生效了,,,由于时间有限,,写的有点仓促了,哪里不懂的可以问我.小伙伴抓紧时间试试吧...
网友评论
private AuthenticationManager myAuthenticationManager; 注入一下.
@primary
public DefaultWebInvocationPrivilegeEvaluator customWebInvocationPrivilegeEvaluator() {
return new DefaultWebInvocationPrivilegeEvaluator(myFilterSecurityInterceptor);
} 不好意思这个忘记写了..现在改正过来了.
:getAttributes(Object object)方法。 自己实现的过滤用户请求类,也可以直接使用 FilterSecurityInterceptor
AbstractSecurityInterceptor有三个派生类: FilterSecurityInterceptor,负责处理FilterInvocation,实现对URL资源的拦截。 MethodSecurityInterceptor,负责处理MethodInvocation,实现对方法调用的拦截。 AspectJSecurityInterceptor,负责处理JoinPoint,主要是用于对切面方法(AOP)调用的拦截。 还可以直接使用注解对Action方法进行拦截,例如在方法上加:@PreAuthorize("hasRole('ROLE_SUPER')")
private void invoke(FilterInvocation fi) throws IOException, ServletException{}
// object为FilterInvocation对象
//super.beforeInvocation(fi);源码
//1.获取请求资源的权限
//执行Collection<ConfigAttribute> attributes = SecurityMetadataSource.getAttributes(object);
//2.是否拥有权限
//this.accessDecisionManager.decide(authenticated, object, attributes);
方法都执行了,还需要判断权限吗?
http://www.yiibai.com/spring-security/spring-security-4-method-security-using-preauthorize-postauthorize-secured-el.html