Spring security 总结

在之前的一个项目中有使用到,方便以后项目框架的搭建,这里还是再总结一下。


Spring Security 的认证流程

在这里插入图片描述

  1. 用户发出登录请求。

  2. 首先经过 SecurityContextPersistenceFilter 过滤器,将 Session 中的认证信息保存到 SecurityContextHodlder 中

  3. 然后在认证逻辑过滤器UsernamePasswordAuthenticationFilter,封装令牌 Token,设置请求信息等。一般我们需要自定义认证逻辑,所以需要继承并重写

  4. 然后通过 AuthenticationManager 认证管理器,遍历所有的 AuthenticationProvider,找到支持该 Token 的认证提供者即 AbstractUserDetailsAuthenticationProvide。

  5. AbstractUserDetailsAuthenticationProvider 会调用它的子类 DaoAuthenticationProvider 的 retrieveUser 方法来获取用户信息 UserDetails。

  6. 而 DaoAuthenticationProvider 会调用 UserDetailsService 接口的 loadUserByUsername 方法来获取用户信息 UserDetails,我们只要实现 UserDetailsService 接口,写获取用户信息的逻辑就可以了

  7. 如果整个过程都没有异常,则认证通过,最终将认证结果 Authentication 保存到 SecurityContext 中,然后将 SecurityContext 保存到 SecurityContextHolder 中。

  8. 再次经过 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"/>
发布了96 篇原创文章 · 获赞 57 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/MOKEXFDGH/article/details/100928421
今日推荐