文章目录
spring security工作原理简介
spring security和springmvc配合使用可以提供安全性,可以通过注册org.springframework.web.filter.DelegatingFilterProxy这个filter进行安全管理,也可以扩展AbstractSecurityWebApplicationInitializer类进行java方式的配置,AbstractSecurityWebApplicationInitializer可以自动配置DelegatingFilterProxy,DelegatingFilterProxy会拦截所有请求并将请求委托给id为springSecurityFilterChain的bean来进行处理(这里细节几乎不需要了解)
样例
WebInitializer启动配置(相当于加入filter启动配置)
public class WebInitializer extends AbstractSecurityWebApplicationInitializer {
}
MVCInitializer启动全局配置
public class MVCInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebMvcConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
WebMvc配置
@Configuration
@EnableWebMvc
@ComponentScan("mvc.security.jsp")
public class WebMvcConfig extends WebMvcConfigurerAdapter {
}
RootConfig配置
@Configuration
// 排除EnableWebMvc注解下的所描路径
@ComponentScan(basePackages = { "mvc.security.jsp" }, excludeFilters = {
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class) })
public class RootConfig {
}
spring security配置(内存用户)
@Configuration
@EnableWebSecurity // 开启spring security配置
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置用户,内存用户
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {// 用户认证配置
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN").and().withUser("user")
.password("user").roles("USER");// 内存用户
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面(如果不开启则默认登陆界面不出现)
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/admin").authenticated()// 只有通过验证的用户才可以访问
.anyRequest().permitAll();// 其余任何请求无需验证
}
}
spring security配置(数据库用户)
实体和dao:
@Entity
public class SysRole {
@Id
@GeneratedValue
private Long id;
private String name;
/*get and set*/
}
@Entity
public class SysUser implements UserDetails {
/**
*
*/
private static final long serialVersionUID = -4452539189946701748L;
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@Transient
private List<GrantedAuthority> auths;
/*get and set*/
}
服务层UserDetailsService
@Component
public class CustomUserService implements UserDetailsService {
@Autowired
private SysUserRepository userRepository;
@Autowired
private SysRoleRepository roleRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userRepository.findByUsername(username);
if (null == user) {
throw new UsernameNotFoundException("'" + username + "'的用户名不存在");
}
List<SysRole> roles = roleRepository.findByUserId(user.getId());
List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
for (SysRole role : roles) {
auths.add(new SimpleGrantedAuthority(role.getName()));
}
user.setAuths(auths);
return user;
}
}
WebSecurityConfig配置:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService customUserService() {
return new CustomUserService();
}
/**
* 配置用户
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {// 用户认证配置
auth.userDetailsService(customUserService());// 自定义数据库用户
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 配置权限和url的映射
*/
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面(如果不开启则默认登陆界面不出现)
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/admin").authenticated()// 只有通过验证的用户才可以访问
.anyRequest().permitAll();// 其余任何请求无需验证
}
}
SecurityContextHolder获取security context
SecurityContextHolder可以获取context从而获取其他信息,比如权限,用户名等
public static String getUserName() {
String userName;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof AnonymousAuthenticationToken) {
userName = (String) authentication.getPrincipal();
} else {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
if (null != userDetails) {
userName = userDetails.getUsername();
} else {
userName = "";
}
}
return userName;
}
用户定义
内存用户可以使用withUser()方法返回UserDetailsBuilder对象,此对象可以定义用户的其他情况,方法如下:
方法 | 描述 |
---|---|
accountExpired(boolean) | 定义账户是否过期 |
accountLocked(boolean) | 定义账户是否锁定 |
and() | 用来连接配置 |
authorities(GrantedAuthority…) | 授予用户一项或多项权限 |
authorities(List<? extends GrantedAuthority>) | 授予用户一项或多项权限 |
authorities(String…) | 授予用户一项或多项权限 |
credentialsExpired(boolean) | 定义凭证是否过期 |
disabled(boolean) | 定义账号是否禁用 |
password(String) | 定义用户密码 |
roles(String) | 定义用户角色 |
数据库用户可以利用UserDetails接口,重写接口中的方法,返回相应数据,可以讲数据存入数据库,在重写方法时查询数据库某一个字段
public abstract interface UserDetails extends Serializable {
public abstract Collection<? extends GrantedAuthority> getAuthorities();
public abstract String getPassword();
public abstract String getUsername();
public abstract boolean isAccountNonExpired();
public abstract boolean isAccountNonLocked();
public abstract boolean isCredentialsNonExpired();
public abstract boolean isEnabled();
}
请求定义
定义请求靠重写configure(HttpSecurity http)来实现,请求定义的核心方法如下:
方法 | 描述 |
---|---|
antMatchers(String[] antPatterns) | 匹配一个或多个url |
antMatchers(HttpMethod method, String[] antPatterns) | 匹配一个或多个url并匹配相应方法,多用于restful请求 |
anyRequest() | 任意请求 |
regexMatchers(String[] antPatterns) | 正则匹配一个或多个url |
regexMatchers(HttpMethod method, String[] antPatterns) | 正则匹配一个或多个url并匹配相应方法,多用于restful请求 |
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/admin").authenticated()
.antMatchers("/bucs",HttpMethod.POST).authenticated()
.regexMatchers("/bucs/.*").authenticated()
.regexMatchers("/aucs/.*",HttpMethod.POST).authenticated()
.anyRequest().permitAll()// 其余任何请求无需验证,anyRequest一定要放最后,先匹配小范围
.and().anonymous();
}
以上方法返回AuthorizedUrl对象,AuthorizedUrl对象用来定义请求是否受限或者不受限,主要方法如下:
方法 | 描述 |
---|---|
access(String) | 给定的SpEL表达式为true,就允许访问 |
anonymous() | 允许匿名用户访问 |
authenticated() | 允许认证通过的用户访问 |
denyAll() | 无条件拒绝所有用户访问 |
fullyAuthenticated() | 完整认证(非remeber-me用户)则允许访问 |
hasAuthority(String) | 具备给定权限则允许访问 |
hasRole(String) | 具备给定角色则允许访问 |
hasAnyAuthority(String[]) | 具备给定权限中的一个则允许访问 |
hasAnyRole(String[]) | 具备给定角色中的一个则允许访问 |
hasIpAddress(String) | 请求来自指定ip则允许访问 |
not() | 对其他方法的结果求反 |
permitAll() | 无条件允许 |
rememberMe() | 允许remeber-me登陆用户访问 |
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/admin").permitAll()//对“/admin不进行认证”
.antMatchers("/bucs",HttpMethod.POST).hasRole("ADMIN")
.regexMatchers("/bucs/.*").hasAuthority("ROLE_ADMIN")//必须加ROLE_
.regexMatchers("/aucs/.*",HttpMethod.POST).authenticated()
.anyRequest().permitAll()// 其余任何请求无需验证
.and().anonymous();
}
https强制安全通道
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().authorizeRequests().antMatchers("/admin").hasRole("ADMIN")
.and().requiresChannel()
.antMatchers("/user").requiresSecure()// 需要安全通道
.antMatchers("/admin").requiresInsecure();// 不需要安全通道,默认情况不需要安全通道
}
跨站请求伪造csrf
开启:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().authorizeRequests().antMatchers("/admin").hasRole("ADMIN")
.and().csrf();
}
关闭:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().authorizeRequests().antMatchers("/admin").hasRole("ADMIN")
.and().csrf().disable();
}
启用remeber-me
这个功能是在在cookie中存储一个token来完成,token中包含用户名、密码、过期时间和一个私钥并且都进行类md5哈希,这里设定token时长为1天,私钥为helloKey
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/admin").hasRole("ADMIN")
.and().rememberMe()//开启remeber-me
.tokenValiditySeconds(24 * 60 * 60 * 1000)//设置token时长
.key("helloKey");//设置key
}
登陆
formLogin()定义登陆
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/loginPage")// 定义登陆界面,这里登陆界面可以使用全url比如:http://localhost:8080/springmvc-security-jsp/loginPage
.permitAll()// 这里一定要加permitAll,否则登陆界面无法访问
.loginProcessingUrl("/login")// 登陆所用的url,即用户名密码等信息提交到的url,默认url是/login,但是自定义页面之后默认url不起作用,这里再写一遍
.successHandler(new AuthenticationSuccessHandler() {//successHandler和defaultSuccessUrl只能配置一个,会相互覆盖,后配置的覆盖先配置的
@Override
public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
throws IOException, ServletException {
System.out.println("登陆成功");
arg1.sendRedirect("loginSuccess");
}
})
// .defaultSuccessUrl("/loginSuccess")// 登陆成功后跳转的url,也可以使用全url
.failureHandler(new AuthenticationFailureHandler(){//failureHandler和failureUrl只能配置一个,会相互覆盖,后配置的覆盖先配置的
@Override
public void onAuthenticationFailure(HttpServletRequest paramHttpServletRequest,
HttpServletResponse paramHttpServletResponse,
AuthenticationException paramAuthenticationException) throws IOException, ServletException {
System.out.println("登陆失败");
paramHttpServletResponse.sendRedirect("loginFailure");
}
})
// .failureUrl("/loginFailure")// 登陆失败后跳转的url,也可以使用全url
.and().logout()// 定义登出
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/loginFailure").permitAll()//这里在定义登陆失败页面时需要不鉴权,否则登陆失败页面无法访问
.antMatchers("/admin").hasRole("ADMIN").anyRequest().authenticated();
}
配置项 | 配置项说明 |
---|---|
loginPage | 登录页面,如果用户未指定,Security将提供默认的登录页面; 默认登录页面请求链接:/login |
usernameParameter | 用户名属性的名称;默认是username |
passwordParameter | 密码属性名称,默认是password |
failureForwardUrl | 授权失败时的跳转链接,实验时配置failureForwardUrl会导致登陆url无效 |
failureUrl | 登录失败时的跳转链接,默认是/login?error |
failureHandler | 登录失败后的处理器 |
successForwardUrl | 授权成功时的跳转链接,实验时配置successForwardUrl会导致登陆url无效 |
successHandler | 登录成功时的处理器 |
defaultSuccessUrl | 指定如果用户登录前未访问需要授权访问的页面,登录成功后的跳转链接 |
loginProcessingUr | l 登录请求处理链接 |
前端代码:
<form name="form" action="login" method="post">
<!-- csrf配置 -->
<sec:csrfInput />
<div class="form-group">
<label for="username">账号</label> <input type="text"
class="form-control" name="username" value="" />
</div>
<div class="form-group">
<label for="password">密码</label> <input type="password"
class="form-control" name="password" value="" />
</div>
<input type="submit" class="btn btn-primary" />
</form>
登出
logout()定义登出,和登入一样可以添加登出后需要做的事情,以及登出后需要跳转的url
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 表示通过登陆来实现验证,spring提供一个简单的默认登陆页面
.and().logout()// 定义登出
// .logoutSuccessUrl("/logoutSuccess").permitAll()//此处一定更要使用permitAll,因为登出后没有权限无法访问登出成功界面,不过一般登出成功界面就是登陆界面在登陆界面permitAll就行
.logoutSuccessHandler(new LogoutSuccessHandler(){//此处会覆盖logoutSuccessUrl的配置,如果使用logoutSuccessHandler需要给url“/logoutSuccess”开通permitAll
@Override
public void onLogoutSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
throws IOException, ServletException {
System.out.println("登出成功");
arg1.sendRedirect("logoutSuccess");
}
}).addLogoutHandler(new LogoutHandler(){//如果登出之前需要处理一些事务,可以在此接口中实现
@Override
public void logout(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2) {
System.out.println("登出前处理");
}
}).clearAuthentication(true)// 用户在退出后清除权限
.invalidateHttpSession(true)// 用户在退出后Http session失效
.and().authorizeRequests()// 开始请求权限配置
.antMatchers("/loginFailure").permitAll()//这里在定义登陆失败页面时需要不鉴权,否则登陆失败页面无法访问
.antMatchers("/logoutSuccess").permitAll()//这里在定义登出成功页面时需要不鉴权,否则登出成功页面无法访问
.antMatchers("/admin").hasRole("ADMIN").anyRequest().authenticated();
}
前端代码:
<form name="form" action="logout" method="post">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" /> <input type="submit"
class="btn btn-primary" />
</form>
方法鉴权
方法一:
配置:
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
添加权限
@Secured(value = { "ROLE_USER" })
public String getAdmin(){
return "admin";
}
方法二:
JSR-250的@RolesAllowed注解
配置:
jsr250Enabled为true可以使用@RolesAllowed注解
@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
添加权限
@RolesAllowed(value = { "ADMIN" })
public String getUser(){
return "user";
}
方法三(使用springEL表达式):
配置:
prePostEnabled为true可以使用以下注解:
@PreAuthorize :在方法调用之前,基于表达式的计算结果来限制对方法的访问
@PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
@PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled= true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
添加权限
@PreAuthorize("hasRole('USER')")//在方法前添加是否有USER角色,方法其实没有被调用
public String getUser(){
return "user";
}
returnObject是springEL表达式的内置对象PostAuthorize中也可以使用returnObject来表示返回值
@PostAuthorize("hasRole('USER')")//在方法前添加是否有USER角色,方法其实被调用了,但是会给浏览器返回403
public String getUser(){
return "user";
}
过滤返回结果,id为1的允许返回
@PostFilter("filterObject.id == 1")
public List<Student> students(){//这里返回值必须是个集合或者数组对象,filterObject是springEL表达式的内置对象,表示过滤对象
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student(1,"123456"));
list.add(new Student(2,"123456"));
return list;
}
过滤入参,id为1的允许进入
@PreFilter(filterTarget = "ids", value = "filterObject == 1")
public String getUser(List<Integer> ids) {//这里入参必须是个集合或者数组对象filterObject也同上
return "user." + ids;
}
自定义权限计算器
配置:
这里需要重写createExpressionHandler方法,设置自定义权限计算器
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = (DefaultMethodSecurityExpressionHandler) super.createExpressionHandler();
handler.setPermissionEvaluator(new StudentPermissionEvaluator());
return handler;
}
}
自定义权限计算器:
public class StudentPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication paramAuthentication, Object targetObjet, Object premission) {
if (paramAuthentication.getName().equals("user")) {
if (premission.equals("show")) {
if (targetObjet.equals(2) || targetObjet.equals(3)) {
return true;
}
}
}
return false;
}
// 如果目标对象的id可以得到,paramSerializable就是id,可以使用这个方法
@Override
public boolean hasPermission(Authentication paramAuthentication, Serializable paramSerializable, String paramString,
Object paramObject) {
throw new UnsupportedOperationException();
}
}
添加权限
@PreFilter("hasPermission(filterObject,'show')")//这里一定要用filterObject
public String getUser(List<Integer> ids) {// 这里必须是个集合或者数组对象
return "user." + ids;
}
前端应用(spring security taglib)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<!-- 需要引入的标签库 -->
<%@ taglib prefix="sec"
uri="http://www.springframework.org/security/tags"%>
<html>
<head>
<base href="<%=basePath%>">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
登陆成功
<br>
<!-- sec:authentication标签显示用户详细信息 -->
<!-- 直接输出 -->
<sec:authentication property="principal.username"/><br>
<!-- 输出到变量,默认的scope在pageContext -->
<sec:authentication property="principal.username" var="username1"/>
<%=pageContext.getAttribute("username1") %><br>
<!-- 输出到变量,定义到request属性中 -->
<sec:authentication property="principal.username" var="username2" scope="request"/>
<%=request.getAttribute("username2") %><br>
<!-- sec:authorize便签通过spEL表达式,决定渲染什么内容 -->
<sec:authorize access="hasRole('ADMIN')">ADMIN user</sec:authorize><br>
<sec:authorize access="hasRole('USER')">USER user</sec:authorize><br>
<sec:authorize access="isAuthenticated() and principal.username=='admin'">复杂表达式</sec:authorize><br>
<!-- 如果此用户有访问/admin的权限,则显示 -->
<sec:authorize url="/admin">通过url权限来决定是否显示</sec:authorize><br>
<form name="form" action="logout" method="post">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" /> <input type="submit"
class="btn btn-primary" />
</form>
</body>
</html>
获取所有已经登陆的用户
获取用户主要使用SessionRegistry来进行,SessionRegistry可以获取session中的所有用户
@Autowired
private SessionRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//注册sessionRegistry并设置最大session个数
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().sessionManagement().maximumSessions(10).sessionRegistry(sessionRegistry);
}
//使用sessionRegistry获取所有用户
sessionRegistry.getAllPrincipals();
解密前端传来的加密后的密码
解密密码主要靠AuthenticationProvider来进行
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
String frountPassword = authentication.getCredentials().toString();//获取前端传递过来的密码(加密形式)
//使用加解密算吗解密密码...
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
@Autowired
private AuthenticationProvider authenticationProvider;
@Bean
public AuthenticationProvider authenticationProvider() {
MyAuthenticationProvider authenticationProvider = new MyAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return authenticationProvider;
}
调试时候遇见的常见错误
302 FOUND:
这种错误是重定向,主要是因为访问的页面没有权限,所以springsecurity重定向到另外一个界面(一般是登陆界面)