在之前的一个项目中有使用到,方便以后项目框架的搭建,这里还是再总结一下。
Spring Security 的认证流程
-
用户发出登录请求。
-
首先经过 SecurityContextPersistenceFilter 过滤器,将 Session 中的认证信息保存到 SecurityContextHodlder 中。
-
然后在认证逻辑过滤器UsernamePasswordAuthenticationFilter,封装令牌 Token,设置请求信息等。一般我们需要自定义认证逻辑,所以需要继承并重写。
-
然后通过 AuthenticationManager 认证管理器,遍历所有的 AuthenticationProvider,找到支持该 Token 的认证提供者即 AbstractUserDetailsAuthenticationProvide。
-
AbstractUserDetailsAuthenticationProvider 会调用它的子类 DaoAuthenticationProvider 的 retrieveUser 方法来获取用户信息 UserDetails。
-
而 DaoAuthenticationProvider 会调用 UserDetailsService 接口的 loadUserByUsername 方法来获取用户信息 UserDetails,我们只要实现 UserDetailsService 接口,写获取用户信息的逻辑就可以了。
-
如果整个过程都没有异常,则认证通过,最终将认证结果 Authentication 保存到 SecurityContext 中,然后将 SecurityContext 保存到 SecurityContextHolder 中。
-
再次经过 SecurityContextPersistenceFilter 过滤器时,将 SecurityContextHolder 中的 SecurityContext 保存到 Session 中,清空 SecurityContextHolder 中的内容,这样就记住了当前用户的登录状态。
Spring 中整合 Spring security
引入 jar 包就不说了,还有角色 Role,权限 Permission这里就不说了。
首先是我们的 User 实体类需要实现 UserDetails 接口,并重写所有方法:
public class User implements UserDetails {
private Long id;
private String email;
private String password;
private String phone;
private String nickName;
private String state;
private String imgUrl;
private String enable;
@Transient
protected List<Role> roles;
//getter/setter方法...
//重写所有接口方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {//返回该用户拥有的权限
if(roles == null || roles.size()<=0){
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
for(Role r:roles){
authorities.add(new SimpleGrantedAuthority(r.getRoleValue()));
}
return authorities;
}
@Override
public String getUsername() {//获取用户名
return email;
}
@Override
public boolean isAccountNonExpired() {// 帐户是否过期
return true;
}
@Override
public boolean isAccountNonLocked() {// 帐户是否被冻结
return true;
}
@Override
public boolean isCredentialsNonExpired() {// 帐户密码是否过期
return true;
}
@Override
public boolean isEnabled() {// 帐号是否可用
if(StringUtils.isNotBlank(state) && "1".equals(state) && StringUtils.isNotBlank(enable) && "1".equals(enable)){
return true;
}
return false;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof User) {
return getEmail().equals(((User)obj).getEmail())||getUsername().equals(((User)obj).getUsername());
}
return false;
}
@Override
public int hashCode() {
return getUsername().hashCode();
}
}
注: User 类修改后,对应功能的 Service 也得修改,因为增加了 List<Role> 属性,所以需要进行联表查询。
实现 UserDetailsService 接口,重写 loadUserByUsername 方法:
public class AccountDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userService.findByEmail(email);//根据 email 获取用户
if(user == null){
throw new UsernameNotFoundException("用户名或密码错误");
}
List<Role> roles = roleService.findByUid(user.getId());
user.setRoles(roles);//设置用户的的角色属性
return user;
}
}
如果我们认证逻辑还需要加上验证码,则需要修改认证逻辑过滤器: 继承 UsernamePasswordAuthenticationFilter 重写 attemptAuthentication 方法。
public class AccountAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private String codeParameter = "code";
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = this.obtainUsername(request);//用户名
String password = this.obtainPassword(request);//密码
String code = request.getParameter(this.codeParameter);//验证码
String caChecode = (String)request.getSession().getAttribute("VERCODE_KEY");
boolean flag = CodeValidate.validateCode(code,caChecode);//校验验证码
if(!flag){
throw new UsernameNotFoundException("验证码错误");
}
if(username == null) {
username = "";
}
if(password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);//token
this.setDetails(request, authRequest);//设置请求信息
//通过 AuthenticationManager 找到支持的 AuthenticationProvider 进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
此外需要设置访问失败,即权限不够的跳转页面: 实现 AccessDeniedHandler 接口,重写 handle 方法
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private String errorPage;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//根据请求头中 X-Requested-With 的属性值是否是 XMLHttpRequest 来判断是不是 AJAX 请求。
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
//如果是 AJAX 请求则返回 JSON 格式数据,并结束方法。
if (isAjax) {
String jsonObject = "{\"message\":\"Access is denied!\",\"access-denied\":true}";
String contentType = "application/json";
response.setContentType(contentType);
PrintWriter out = response.getWriter();
out.print(jsonObject);
out.flush();
out.close();
return;
} else {
//如果不是 AJAX 请求。再判断 errorPage 是否为空,如果不为空,则设置状态码为403,并转发到配置的错误页面,如果 errorPage 为空,则直接返回403错误页面
if (!response.isCommitted()) {
if (this.errorPage != null) {
request.setAttribute("SPRING_SECURITY_403_EXCEPTION", e);
response.setStatus(403);
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPage);
dispatcher.forward(request, response);
} else {
response.sendError(403, e.getMessage());
}
}
}
}
public void setErrorPage(String errorPage) {//获取配置文件中 errorPage 的路径
if(errorPage != null && !errorPage.startsWith("/")) {
throw new IllegalArgumentException("errorPage must begin with '/'");
} else {
this.errorPage = errorPage;
}
}
}
使用时还需要记得在 web.xml 加载 sercurity 配置文件,并配置权限过滤器链:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
而在我们需要获取当前用户时,可以这样获取:
public User getCurrentUser(){
User user = null;
Authentication authentication = null;
SecurityContext context = SecurityContextHolder.getContext();
if(context!=null){
authentication = context.getAuthentication();
}
if(authentication!=null){
Object principal = authentication.getPrincipal();
//如果是匿名用户
if(authentication.getPrincipal().toString().equals( "anonymousUser" )){
return null;
}else {
user = (User)principal;
}
}
return user;
}
最后贴上 sercurity 的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 静态资源、登录等 不拦截 -->
<security:http security="none" pattern="/css/**" />
<security:http security="none" pattern="/js/**" />
<security:http security="none" pattern="/images/**" />
<security:http security="none" pattern="/favicon.ico"/>
<security:http security="none" pattern="/login*" />
<security:http security="none" pattern="/checkCode"/>
<security:http security="none" pattern="/checkEmail"/>
<!--
配置具体的规则
auto-config="true" 不用自己编写登录的页面,框架提供默认登录页面
use-expressions="false" 是否使用SPEL表达式
-->
<security:http auto-config="false" access-decision-manager-ref="accessDecisionManager"
use-expressions="true" entry-point-ref="loginEntryPoint">
<security:headers>
<security:frame-options disabled="true"></security:frame-options>
</security:headers>
<!-- 配置页面信息 -->
<security:form-login login-page="/login" authentication-failure-url="/login?error=1"
login-processing-url="/doLogin" password-parameter="password"
default-target-url="/list"
username-parameter="username" />
<!-- 将 frame-options 设置为禁用,否则浏览器拒绝当前页面加载任何 Frame 页面。如果不加如下设置,上传图片时会超时: -->
<security:access-denied-handler ref="accessDeniedHandler" />
<!-- 关闭跨域请求 -->
<security:csrf disabled="true"/>
<!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_USER的角色" -->
<security:intercept-url pattern="/" access="permitAll"/>
<security:intercept-url pattern="/index**" access="permitAll"/>
<security:intercept-url pattern="/sendSms" access="permitAll"/>
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
<!-- session失效url session策略-->
<security:session-management invalid-session-url="/index.jsp" session-authentication-strategy-ref="sessionStrategy">
</security:session-management>
<!-- spring-security提供的过滤器 以及我们自定义的过滤器 authenticationFilter-->
<security:custom-filter ref="logoutFilter" position="LOGOUT_FILTER" />
<security:custom-filter before="FORM_LOGIN_FILTER" ref="authenticationFilter"/>
<security:custom-filter after="FORM_LOGIN_FILTER" ref="phoneAuthenticationFilter"/>
<security:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/>
</security:http>
<!-- 我们的 MyAccessDeniedHandler -->
<bean id="accessDeniedHandler"
class="moke.demo.ssm.security.account.MyAccessDeniedHandler">
<property name="errorPage" value="/accessDenied.jsp" />
</bean>
<!-- 认证管理器,使用自定义的 UserService,并对密码采用md5加密 -->
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider user-service-ref="accountService">
<security:password-encoder hash="md5">
<security:salt-source user-property="username"></security:salt-source>
</security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<!-- 自定义的过滤器 AccountAuthenticationFilter
登录URL、认证管理器、Session策略、认证成功处理器和认证失败处理器
-->
<bean id="authenticationFilter" class="moke.demo.ssm.security.account.AccountAuthenticationFilter">
<property name="filterProcessesUrl" value="/doLogin"></property>
<property name="authenticationManager" ref="authenticationManager"></property>
<property name="sessionAuthenticationStrategy" ref="sessionStrategy"></property>
<property name="authenticationSuccessHandler">
<bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/list"></property>
</bean>
</property>
</bean>
<!-- 配置登出后的处理 -->
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<!-- 处理退出的虚拟url -->
<property name="filterProcessesUrl" value="/loginout" />
<!-- 退出处理成功后的默认显示url -->
<constructor-arg index="0" value="/login?logout" />
<constructor-arg index="1">
<!-- 退出成功后的handler列表 -->
<array>
<bean id="securityContextLogoutHandler"
class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
</array>
</constructor-arg>
</bean>
<!-- Session策略 -->
<bean id="sessionStrategy" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<constructor-arg>
<list>
<bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<property name="maximumSessions" value="1"></property>
<property name="exceptionIfMaximumExceeded" value="false"></property>
<constructor-arg ref="sessionRegistry"/>
</bean>
<bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy"/>
<bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<constructor-arg ref="sessionRegistry"/>
</bean>
</list>
</constructor-arg>
</bean>
<!-- ConcurrentSessionFilter过滤器配置(主要设置账户session过期路径) -->
<bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">
<constructor-arg ref="sessionRegistry"></constructor-arg>
<constructor-arg value="/login?error=expired"></constructor-arg>
</bean>
<!-- 判断是否过期以及刷新最后一次方法时间 -->
<bean id="sessionRegistry" scope="singleton" class="org.springframework.security.core.session.SessionRegistryImpl"></bean>
<bean id="accountService" class="moke.demo.ssm.security.account.AccountDetailsService"/>