SpringSecurity用户类
/**
* SpringSecurity中的用户实体类
*
*/
public class SecurityUser implements UserDetails {
private static final long serialVersionUID = 1L;
private final String uid;
private final String username;
private final String password;
private final boolean enabled;
private final Collection<? extends GrantedAuthority> authorities;
public SecurityUser(
String uid,
String username,
String password,
boolean enabled,
Collection<? extends GrantedAuthority> authorities) {
this.uid = uid;
this.username = username;
this.password = password;
this.enabled = enabled;
this.authorities = authorities;
}
/**
* 返回分配给用户的角色列表
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
public String getUid() {
return uid;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
/**
* 账户是否激活
*
* @return
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return enabled;
}
/**
* 账户是否未过期
*
* @return
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否未锁定
*
* @return
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密码是否未过期
*
* @return
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
- boolean enabled 是否可用
- boolean accountNonExpired 账户是否失效
- boolean credentialsNonExpired 密码是否失效
- boolean accountNonLocked 账户是否锁定
/**
* SpringSecurity用户工厂类
*
*/
public final class SecurityUserFactory {
private SecurityUserFactory() {
}
/**
* 通过管理员Admin,生成一个SpringSecurity用户
*
* @param admin
* @return
*/
public static SecurityUser create(Admin admin) {
boolean enabled = admin.getStatus() == EStatus.ENABLE;
return new SecurityUser(
admin.getUid(),
admin.getUserName(),
admin.getPassWord(),
enabled,
mapToGrantedAuthorities(admin.getRoleNames())
);
}
private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
在工厂类SecurityUserFactory
中通过管理员Admin生成SpringSecurity用户,并判断用户是否处于正常状态
注销功能
注意:一旦开启了csrf防护功能,logout处理器便只支持POST请求方式了!修改header.jsp中注销请求:
<form action="${pageContext.request.contextPath}/logout" method="post">
<security:csrfInput/>
<input type="submit" value="注销">
</form>
记住我功能
流程分析
上一篇入门篇中,分析源码到认证成功时调用了AbstractRememberMeServices#loginSuccess
方法
// AbstractAuthenticationProcessingFilter.java
// 登陆成功,调用rememberMeServices.loginSuccess
this.rememberMeServices.loginSuccess(request, response, authResult);
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
// 是否勾选记住我
// this.parameter : private String parameter = "remember-me";
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
// 勾选,从这里进入PersistentTokenBasedRememberMeServices.onLoginSuccess方法
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
// 提交的属性名必须叫"remember-me"
String paramValue = request.getParameter(parameter);
// 属性的值可以为 : true, on, yes, 1。
if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
}
return false;
}
}
}
继续进入PersistentTokenBasedRememberMeServices#onLoginSuccess
方法
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
// 获取用户名
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
// 创建记住我的token
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
// 将token持久化到数据库中
this.tokenRepository.createNewToken(persistentToken);
// 将token写入浏览器Cookie中
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
}
页面代码
<%@taglib prefix="security" "uri='http://www.springframework.org/ security /tags"%>
<form action="${pageContext.request.contextPath}/login" method="post">
<!--在认证from表单内携带token,需要在文件头添加SpringSecurity标签库-->
<!--<security:csrfInput/>-->
<div class="form-group has-feedback">
<input type="text" name="username" class="form-control" laceholder="用户名">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" name="password" class="form-control" placeholder="密码">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div>
<div>
<!--name和value属性不能写错-->
<label><input type="checkbox" name="remember-me" value="true"/>记住我,下次自动登录</label>
</div>
</div>
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
</div>
<!-- /.col -->
</div>
</form>
这是因为remember me功能使用的过滤器RememberMeAuthenticationFilter默认是不开启的!
开启remember me过滤器
<security:http auto-config="true" use-expressions="true">
...
<!--开启remember-me过滤器,设置token存储时间为60s-->
</security:remember-me token-validity-seconds="60"/>
</security:http>
remember me安全性分析
记住我功能方便是大家看得见的,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且 Cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么 这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。
此外,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个 无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录 时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。
remember me信息的持久化
创建persistent_logins
表,表名称和字段都是固定的,不能修改
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
修改spring-security.xml
<!--
开启remember me过滤器,
data-source-ref="dataSource" 指定数据库连接池
token-validity-seconds="60" 设置token存储时间为60秒 可省略
remember-me-parameter="remember-me" 指定记住的参数名 可省略
-->
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="60" remember-me-parameter="remember-me"/>
显示当前认证信息
在header.jsp
中找到页面头部最右侧图片处添加如下信息:
<span class="hidden-xs">
<security:authentication property="principal.username" />
</span>
或者
<span class="hidden-xs">
<security:authentication property="name" />
</span>
在页面控制资源权限
<security:authorize access="hasAnyRole('ROLE_ADMIN')">
<!--只有ROLE_ADMIN权限的用户可见标签里的内容-->
</ecurity:authorize>
<security:authorize access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')">
<!--有ROLE_ADMIN或者ROLE_USER权限的用户都可见标签里的内容-->
</ecurity:authorize>
授权操作
IOC容器介绍
我们的spring-security.xml需要放到父容器中被保护起来,不能放到子容器中被直接访问
说明:SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在 controller类中,对应注解支持应该放在mvc配置文件中,因为controller类是有mvc配置文件扫描并创建的,同 理,注解放在service类中,对应注解支持应该放在spring配置文件中。由于我们现在是模拟业务操作,并没有 service业务代码,所以就把注解放在controller类中了。
开启授权的注解支持
在服务器端我们可以通过Spring security提供的注解对方法来进行权限控制。Spring Security在方法的权限控制上
支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解,这三种注解默认都是没有启用的,需要
单独通过global-method-security元素的对应属性进行启用
三类注解,但实际开发中,用一类即可!
方法一:
spring-security.xml添加
<!--
开启权限控制注解支持
jsr250-annotations="enabled" 表示支持jsr250-api的注解,需要jsr250-api的jar包
pre-post-annotations="enabled" 表示支持spring表达式注解
secured-annotations="enabled" 这才是SpringSecurity提供的注解
-->
<!--这里使用JSR-250注解-->
<security:global-method-security jsr250-annotations="enabled"
pre-post-annotations="enabled"
secured-annotations="enabled"/>
方法二:
注解开启
@EnableGlobalMethodSecurity :Spring Security默认是禁用注解的,要想开启注解,需要在继承
WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,并在该类中将
AuthenticationManager定义为Bean。这个再Springboot项目中再学习
JSR-250注解简单介绍
pom.xml文件中需要导入依赖
- @RolesAllowed表示访问对应方法时所应该具有的角色
示例:
@RolesAllowed({“USER”, “ADMIN”})
该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省
略前缀ROLE_,实际的权限可能是ROLE_ADMIN - @PermitAll表示允许所有的角色进行访问,也就是说不进行权限控制
- @DenyAll是和PermitAll相反的,表示无论什么角色都不能访问
在注解支持对应类或者方法上添加注解
@Controller
@RequestMapping("/product")
public class ProductController {
@Autowired
private IProductService productService;
//查询全部产品
@RequestMapping("/findAll.do")
@RolesAllowed("ADMIN")
public ModelAndView findAll() throws Exception {
ModelAndView mv = new ModelAndView();
List<Product> ps = productService.findAll();
mv.addObject("productList", ps);
mv.setViewName("product-list1");
return mv;
}
}
也可以使用多种注解的方式
@Controller
@RequestMapping("/product")
public class ProductController {
//@Secured({"ROLE_PRODUCT","ROLE_ADMIN"})//springSecurity内部制定的注解
//@RolesAllowed({"ROLE_PRODUCT","ROLE_ADMIN"})//jsr250注解
@PreAuthorize("hasAnyAuthority('ROLE_PRODUCT','ROLE_ADMIN')")//spring的el表达式注解
@RequestMapping("/findAll")
public String findAll(){
return "product-list";
}
}
权限不足异常处理
403异常
方式一:在spring-security.xml配置文件中处理
<security:http auto-config="true" use-expressions="true">
...
<!--403异常处理-->
<security:access-denied-handler error-page="/403.jsp"/>
</security:http>
方式二:在web.xml中处理
<error-page>
<error-code>403</error-code>
<location>/403.jsp</location>
</error-page>
方式三:编写异常处理器
拦截器和过滤器的区别
- 拦截器:可以在Spring中进行使用
- 过滤器:只能在web.xml中进行配置,也就是只能在web工程中使用
或者我们可以实现一个Spring给我们提供好的接口
@Component
public class HandlerControllerException implements HandlerExceptionResolver {
/**
* @param httpServletRequest
* @param httpServletResponse
* @param o 出现异常的对象
* @param e 出现的异常信息
* @return ModelAndView
*/
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
ModelAndView mv = new ModelAndView();
//将异常信息放入request域中,基本不用
mv.addObject("errorMsg", e.getMessage());
//指定不同异常跳转的页面
if(e instanceof AccessDeniedException){
mv.setViewName("redirect:/403.jsp");
}else {
mv.setViewName("redirect:/500.jsp");
}
return mv;
}
}
下面一个更简单的方式,通过注解就相当于我们实现了 HandlerExceptionResolver
@ControllerAdvice
public class HandlerControllerAdvice{
@ExceptionHandler(AccessDeniedException.class)
public String handlerException(){
return "redirect:/403.jsp";
}
@ExceptionHandler(RuntimeException.class)
public String runtimeHandlerException(){
return "redirect:/500.jsp";
}
}