基于Spring+SpringMVC+MyBatis博客系统的开发教程(十五)

第15课:Spring Security 源码解读及账号登录认证授权

Spring Security 简介

Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC,DI(IoC:控制反转 Inversion of Control,DI:依赖注入 Dependency Injection)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

——来源百度百科

在做 Spring Security 整合之前,先带大家看下 Spring Security 的源码,了解其实现原理。

Spring Security 源码解读

Spring Security 源码下载

Spring Security 源码下载地址见下:

https://github.com/spring-projects/spring-security

大家可以用 Git 把项目克隆到本地或者直接下载 Zip 源码包,如图:

源码解读

Spring Security 项目源码下载完成以后,用 IDE 打开该项目。

1. 使用 Ctrl+Shift+N 组合键查找 UsernamePasswordAuthenticationFilter 过滤器,该过滤器是用来处理用户认证逻辑的,进入后如图:

(1)可以看到它默认的登录请求 URL 是 /login,并且只允许 POST 方式的请求。

(2)obtainUsername() 方法点进去发现它默认是根据参数名为 username 和 password 来获取用户名和密码的。

(3)通过构造方法实例化一个 UsernamePasswordAuthenticationToken 对象,此时调用的是 UsernamePasswordAuthenticationToken 的两个参数的构造函数,如果点击进不去,可直接用 Ctrl+Shift+N 查找(等依赖自动下载完成就可以跟进了),如图:

其中 super(null) 调用的是父类的构造方法,传入的是权限集合,因为目前还没有认证通过,所以不知道有什么权限信息,这里设置为 null,然后将用户名和密码分别赋值给 principal 和 credentials,同样因为此时还未进行身份认证,所以 setAuthenticated(false)。

(4)setDetails(request, authRequest) 是将当前的请求信息设置到 UsernamePasswordAuthenticationToken 中。

(5)通过调用 getAuthenticationManager() 来获取 AuthenticationManager,通过调用它的 authenticate 方法来查找支持该 token(UsernamePasswordAuthenticationToken) 认证方式的 provider,然后调用该 provider 的 authenticate 方法进行认证)。

2. AuthenticationManager 是用来管理 AuthenticationProvider 的接口,通过查找后进入,然后使用 Ctrl+H 组合键查看它的继承关系,找到 ProviderManager 实现类,它实现了 AuthenticationManager 接口,查看它的 authenticate 方法,它里面有段这样的代码:

    for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
        ...
        try {
                result = provider.authenticate(authentication);
        ...
        }
    }

通过 for 循环遍历 AuthenticationProvider 对象的集合,找到支持当前认证方式的 AuthenticationProvider,找到之后调用该 AuthenticationProvider 的 authenticate 方法进行认证处理:

    result = provider.authenticate(authentication);

3. AuthenticationProvider 接口,就是进行身份认证的接口,它里面有两个方法:authenticate 认证方法和 supports 是否支持某种类型 Token 的方法,通过 Ctrl+H 查看继承关系,找到 AbstractUserDetailsAuthenticationProvider 抽象类,它实现了 AuthenticationProvider 接口,它的 supports 方法如下:

        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
            }

说明它是支持 UsernamePasswordAuthenticationToken 类型的 AuthenticationProvider。

再看它的 authenticate 认证方法,其中有一段这样的代码:

 boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
        ...
        }

如果从缓存中没有获取到 UserDetails,那么它调用 retrieveUser 方法来获取用户信息 UserDetails,这里的 retrieveUser 是抽象方法,等一会我们看它的子类实现。

用户信息 UserDetails 是个接口,我们进入查看,它包含以下6个接口方法:

    Collection<? extends GrantedAuthority> getAuthorities();//获取权限集合
    String getPassword();  //获取密码
    String getUsername();   //获取用户名
    boolean isAccountNonExpired(); //账户未过期
    boolean isAccountNonLocked();   //账户未锁定
    boolean isCredentialsNonExpired(); //密码未过期
    boolean isEnabled();    //账户可用

查看它的继承关系发现 User 类实现了该接口,并实现了该接口的所有方法。

接着 AbstractUserDetailsAuthenticationProvider 往下看,找到下面的代码:

 preAuthenticationChecks.check(user);
 additionalAuthenticationChecks(user,
 (UsernamePasswordAuthenticationToken) authentication);

preAuthenticationChecks 预检查,在最下面的内部类 DefaultPreAuthenticationChecks 中可以看到,它会检查上面提到的三个 Boolean 方法,即检查账户未锁定、账户可用、账户未过期,如果上面的方法只要有一个返回 false,就会抛出异常,那么认证就会失败。

additionalAuthenticationChecks 是附加检查,是个抽象方法,等下看子类的具体实现。

下面还有个 postAuthenticationChecks.check(user) 后检查,在最下面的 DefaultPostAuthenticationChecks 内部类中可以看到,它会检查密码未过期,如果为 false 就会抛出异常。

如果上面的检查都通过并且没有异常,表示认证通过,会调用下面的方法:

    createSuccessAuthentication(principalToReturn, authentication, user);

跟进发现此时通过构造方法实例化对象 UsernamePasswordAuthenticationToken 时,调用的是三个参数的构造方法:

        public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

此时会调用父类的构造方法设置权限信息,并调用父类的 setAuthenticated(true) 方法,到这里就表示认证通过了。

下面我们看看 AbstractUserDetailsAuthenticationProvider 的子类,同样 Ctrl+H 可查看继承关系,找到 DaoAuthenticationProvider。

4. DaoAuthenticationProvider 类。

(1)查看 additionalAuthenticationChecks 附加检查方法,它主要是检查用户密码的正确性,如果密码为空或者错误都会抛出异常。

(2)获取用户信息 UserDetails 的 retrieveUser 方法,主要看下面这段代码:

    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

它调用了 getUserDetailsService 先获取到 UserDetailsService 对象,通过调用 UserDetailsService 对象的 loadUserByUsername 方法获取用户信息 UserDetails。

找到 UserDetailsService,发现它是一个接口,查看继承关系,有很多实现,都是 Spring Security 提供的实现类,并不能满足我们的需要,我们想自己制定获取用户信息的逻辑,所以我们可以实现这个接口。比如从我们的数据库中查找用户信息。

5. SecurityContextPersistenceFilter 过滤器。

那么用户认证成功之后,又是怎么保存认证信息的呢,在下一次请求过来时如何判断该用户是否已经认证了呢?

请求进来时会经过 SecurityContextPersistenceFilter 过滤器,进入 SecurityContextPersistenceFilter 过滤器并找到以下代码:

    SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

从 Session 中获取 SecurityContext 对象,如果没有就实例化一个 SecurityContext 对象:

    SecurityContextHolder.setContext(contextBeforeChainExecution);

将 SecurityContext 对象设置到 SecurityContextHolder 中:

    chain.doFilter(holder.getRequest(), holder.getResponse());

表示放行,执行下一个过滤器。

执行完后面的过滤并经过 Servlet 处理之后,响应给浏览器之前再次经过此过滤器。查看以下代码:

    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
    SecurityContextHolder.clearContext();
    this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());

通过 SecurityContextHolder 获取 SecurityContext 对象,然后清除 SecurityContext,最后将获取的 SecurityContext 对象放入 Session 中。

其中 SecurityContextHolder 是与 ThreadLocal 绑定的,即本线程内所有的方法都可以获得 SecurityContext 对象,而 SecurityContext 对象中包含了 Authentication 对象,即用户的认证信息,Spring Security 判断用户是否认证主要是根据 SecurityContext 中的 Authentication 对象来判断。Authentication 对象的详细信息如图:

最后整个过程的流程大致如下图:

Spring Security 在项目中的应用

本节课主要介绍账号登录的认证授权。

为了提高代码的可读性以及可维护性,我们将不同方式的认证授权放在不同的包下。

首先在 wang.dreamland.www 包下新建 security 包,然后在 security 包下新建 account、phone 和 open ,分别代表账号登录、手机登录和第三方登录的认证授权。

账号登录认证授权

1. 实现 UserDetails 接口。

通过上面的源码分析,框架需要的是 UserDetails 类型的实体,所以我们要将实体类 User 实现 UserDetails 接口,然后重写它的所有方法:

public class User implements UserDetails{

    @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;
    }

    public String getPassword() {
        return password;
    }

    @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();
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", email='" + email + '\'' +
                ", password='" + password + '\'' +
                ", phone='" + phone + '\'' +
                ", nickName='" + nickName + '\'' +
                ", state='" + state + '\'' +
                ", imgUrl='" + imgUrl + '\'' +
                ", enable='" + enable + '\'' +
                ", roles=" + roles +
                '}';
    }

    }

代码解读如下:

(1)其中 getAuthorities 方法是获取用户角色信息的方法,用于授权。不同的角色可以拥有不同的权限。

(2)账户未过期、账户未锁定和密码未过期我们这里没有用到,直接返回 True,你也可以根据自己的应用场景写自己的业务逻辑。

(3)为了区分是否是同一个用户,重写 equals 和 hashCode 方法。

实现 UserDetailsService 接口

通过上面的源码分析知道,Spring Security 提供的获取用户信息的方式不满足我们的需求,所以我们自己制定获取用户信息的方式。

1. 准备。

(1)因为不仅要获取用户的基本信息,还要获取用户的角色信息,所以在 RoleService 中添加根据用户 id 获取角色列表的方法:

     /**
     * 根据用户id查询所有角色
     * @param uid
     * @return
     */
    List<Role> findByUid(Long uid);

(2)因为要通过子查询的方式查询角色信息,所以通用 mapper 满足不了需求,需要手写 XML,在 RoleMapper 中添加下面的接口方法:

    /**
     * 根据用户id查询角色信息
     * @param uid
     * @return
     */
    List<Role> findByUid(@Param("uid")Long uid);

(3)在 resources/mapping 下新建 role.xml 文件,配置如下:

<?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="wang.dreamland.www.dao.RoleMapper">
    <select id="findByUid"  resultMap="roleListMap">
      select id,role_name,role_value,enabled from role where id in(select r.r_id from user u, role_user r where u.id = r.u_id and r.u_id = #{uid}) and enabled =1
    </select>

    <resultMap type="wang.dreamland.www.entity.Role" id="roleListMap">
        <id property="id" column="id" />
        <result property="roleName" column="role_name" />
        <result property="roleValue" column="role_value" />
        <result property="enabled" column="enabled" />
    </resultMap>
    </mapper>

查询 SQL 语句,如下:

    select id,role_name,role_value,enabled from role where id in(select r.r_id from user u, role_user r where u.id = r.u_id and r.u_id = #{uid}) and enabled =1

其中括号内的是根据两表关联查询,根据用户 id 查询出所有角色的 id,然后将查询结果当作外层查询语句的查询条件,根据所有角色 id 查询出角色列表,其中 enabled=1 代表查询出可用的角色。其它标签含义之前已经介绍过,这里不再赘述。

(4)在 RoleServiceImpl 实现类中,实现 RoleService 接口中的方法:

    @Override
    public List<Role> findByUid(Long uid) {
        return roleMapper.findByUid(uid);
    }

2. 在 account 包下新建 AccountDetailsService 并实现 UserDetailsService 接口:

 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);
        if(user == null){
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        List<Role> roles = roleService.findByUid(user.getId());
        user.setRoles(roles);

        return user;
    }
    }

代码解读如下:

(1)通过 Autowired 注解注入 UserService 和 RoleService 对象。

(2)根据 email 查询用户,如果为 null 直接抛出异常。

(3)如果不为 null,则根据用户 id 查询角色列表,将角色信息添加到 User 中。

(4)将 user 返回。

3.继承 UsernamePasswordAuthenticationFilter 过滤器

通过源码分析得知 UsernamePasswordAuthenticationFilter 是处理用户认证逻辑的过滤器,继承它可制定自己的认证逻辑,比如加上验证码的验证。

在 account 包下新建 AccountAuthenticationFilter 并继承 UsernamePasswordAuthenticationFilter:

 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 = this.obtainCode(request);
        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);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainCode(HttpServletRequest request) {
        return request.getParameter(this.codeParameter);
    }

    }

代码解读如下:

(1)调用父类的方法获取用户名和密码,默认请求参数是 username 和 password。

(2)调用自定义方法 obtainCode 获取验证码 code,请求参数是 code。

(3)获取 Session 中存入的验证码 caCheCode,key 是 VERCODE_KEY

(4)为了实现可重用,我将判断验证码是否以正确的方法封装到了common 包下的 CodeValidate 类中了,方法很简单:

     public static boolean validateCode(String code,String cacheCode){
        if(StringUtils.isNotBlank(code) && code.equals(cacheCode)){
            return true;
        }
        return false;
    }

(5)如果验证码校验失败则直接抛出异常,认证失败。

(6)下面的代码和源码中一致,将用户名和密码封装到 UsernamePasswordAuthenticationToken 对象中,然后设置请求信息,找到支持的 AuthenticationProvider 进行认证。

实现 AccessDeniedHandler 接口

用户在未登录的情况下访问受保护资源时,会直接跳转到登录页面(不包含 AJAX 请求),这种情况不用考虑。

但是当用户登录之后访问未保护资源时,它默认会返回403的错误页面,这不是我们想要的结果。我们通过实现 AccessDeniedHandler 接口可以让它跳转到指定页面,比如跳转到 accessDenied.jsp,提示用户没有访问权限等。

在 account 包下新建 MyAccessDeniedHandler 并实现 AccessDeniedHandler 接口:

 public class MyAccessDeniedHandler implements AccessDeniedHandler {
    private String errorPage;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        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 {
            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) {
        if(errorPage != null && !errorPage.startsWith("/")) {
            throw new IllegalArgumentException("errorPage must begin with '/'");
        } else {
            this.errorPage = errorPage;
        }
    }
    }

代码解读,如下:

(1)主要通过 set 方法获取配置文件中配置的 errorPage 路径。

(2)根据请求头中 X-Requested-With 的属性值是否是 XMLHttpRequest 来判断是不是 AJAX 请求。

(3)如果是 AJAX 请求则返回 JSON 格式数据,并结束方法。

(4)如果不是 AJAX 请求。再判断 errorPage 是否为空,如果不为空,则设置状态码为403,并转发到配置的错误页面,如果 errorPage 为空,则直接返回403错误页面。

spring-security.xml 配置文件的配置

spring-security.xml 配置文件内容很多。这里就不粘贴了,详见百度网盘,说几个重点配置(未按照顺序)。

1. 对静态资源和都可以访问的资源不拦截,比如:CSS、JS 等静态资源,登录、注册等都可以访问的路径。

    <security:http security="none" pattern="/css/**" />
    <security:http security="none" pattern="/login*" />
    <security:http security="none" pattern="/register*" />

2. 对访问路径不拦截的还有下面的配置,permitAll 允许所有,但是它会经过过滤器,可以获取到用户信息。上面的 none 不经过过滤器直接放行,所以获取不到用户信息:

    <security:intercept-url pattern="/index**" access="permitAll"/>

3. 除了设置为 none 和 permitAll 的访问路径,其它都会进行拦截,并且必须具备 ROLE_USER 权限才能访问,注意 Spring Security 的角色信息要以 ROLE_ 开头。

    <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>

4. AccessDecisionManager 访问决策管理器是一组投票器的集合,默认的策略是使用一个 AffirmativeBased,既只要有一个投票器通过验证就允许用户访问。

 <security:http auto-config="false" access-decision-manager-ref="accessDecisionManager"
                    use-expressions="true" entry-point-ref="loginEntryPoint">


    </security:http>

    <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <constructor-arg>
            <list>
                <ref local="roleVoter"/>
                <ref local="authenticatedVoter"/>
                <ref local="expressionVoter"/>
            </list>
        </constructor-arg>
    </bean>

5. 将 frame-options 设置为禁用,否则浏览器拒绝当前页面加载任何 Frame 页面。如果不加如下设置,上传图片时会超时:

     <security:headers>
            <security:frame-options disabled="true"></security:frame-options>
     </security:headers>

6. 配置登录页信息,分别为登录 URL、认证失败跳转 URL、认证成功跳转 URL、登录 URL、password 和 username 请求参数名称:

    <security:form-login login-page="/login" authentication-failure-url="/login?error=1"
                              default-target-url="/list" login-processing-url="/doLogin"
                             password-parameter="password" username-parameter="username" />

7. 配置我们上面实现的 MyAccessDeniedHandler 处理器:

     <security:access-denied-handler ref="accessDeniedHandler" />
     <bean id="accessDeniedHandler"
                class="wang.dreamland.www.security.account.MyAccessDeniedHandler">
        <property name="errorPage" value="/accessDenied.jsp" />
    </bean>

errorPage 我配置的是 accessDenied.jsp,在 webapp 目录下新建 accessDenied.jsp,我只简单提示无访问权限:

    <%@ page contentType="text/html;charset=UTF-8"%>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <span style="color: red">无访问权限</span>
    </body>
    </html>

8. 在用户未登录时,如果访问受保护资源,会跳转到下面配置的默认登录页:

     <bean id="loginEntryPoint"
        class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
        <!-- 默认登录页的url -->
        <constructor-arg value="/login?error=login"/>
    </bean>

9. 认证管理器 authenticationManager,使用自定义的 accountService,并对密码采用 MD5 加密,注意这里使用的是 Spring Security 提供的 MD5 加密工具,盐 salt 改为对应的用户名。

 <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>

    <bean id="accountService" class="wang.dreamland.www.security.account.AccountDetailsService"/>

10. 配置我们上面自定义的过滤器 AccountAuthenticationFilter,属性依次为登录 URL、认证管理器、Session策略、认证成功处理器和认证失败处理器。认证成功默认跳转 /list,认证失败跳转到登录。

       <bean id="authenticationFilter" class="wang.dreamland.www.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>
        <property name="authenticationFailureHandler">
            <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login?error=fail"></property>
            </bean>
        </property>
    </bean>

其它未列出的请参考配置文件中的注释。

web.xml 配置

1. 在 web.xml 中配置 Spring Security 的权限过滤器链:

    <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>

2. 在 web.xml 中引入 spring-security.xml:

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
          classpath:spring-mybatis.xml,
          classpath:applicationContext-redis.xml,
          classpath:applicationContext-activemq.xml,
          classpath:applicationContext-solr.xml,
          classpath:spring-security.xml
        </param-value>
    </context-param>
启动测试

现在我们配置的是根据 Spring Security 提供的 MD5 加密方式生成的密码,如果测试登录肯定会失败。我们可以通过单元测试或者 main 方法根据用户名生成对应的 MD5 密码。

1. 生成 MD5 密码。

        public static void main(String[] args) {
            String password = new Md5PasswordEncoder().encodePassword("123456", "123456@qq.com");
            System.out.println(password);
    }

比如我的数据库中有个 123456@qq.com 的用户,根据 Spring Security 提供的 MD5 加密工具生成对应的密码,原密码是:123456 , 盐salt是用户邮箱即:123456@qq.com,加密后的密码就是:

794ad2ea7aab3848080cffcd2968a212

将此密码替换掉数据库该用户对应的密码,目前只是测试使用,之后用户注册的时候加密方式也要替换成 Spring Security 提供的 MD5 加密方式。

2. 启动 Tomcat,点击登录,输入错误的密码会发现又跳转到了登录页面。输入正确的密码后跳转到了无访问权限页面。因为我们之前配置中除了放行的路径,其它访问路径都需要 ROLE_USER 角色才能访问。

3. 给登录用户赋予 ROLE_USER 角色。

首先在 role 表中添加一条数据,如下:

然后在中间表 role_user 添加用户和角色对应信息,即用户 id 为1的用户对应的角色 id 为1的角色:

这样用户 id 为1的用户就用了 ROLE_USER 角色所拥有的权限。

4. 再次输入正确的密码访问,这次没有跳转到无访问权限页面而是跳转到了登录页面。原因是之前从 Session 中取的用户,判断为空后又跳转到了登录页面。

解决方法是:我们知道现在用户的认证信息都存在了 SecurityContext 中,我们可以通过 SecurityContext 获取,在 BaseController 中添加获取当前用户的方法:

  /**
     * 获取当前用户
     * @return
     */
    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;
    }

方法很简单,就是从 SecurityContextHolder 中拿到 SecurityContext 对象,然后从 SecurityContext 中拿到 Authentication 对象,通过 Authentication 对象的 getPrincipal() 就可以获取到用户信息。中间做了一些非空判断等。

用户登录成功默认是跳转到 /list 的路径,所以在 PersonalController 中找到该映射 URL 对应的方法,将获取用户的方法改成如下:

    User user = getCurrentUser();

注意其他获取用户的方法都要改成上面的形式,否则获取不到用户信息。

重新启动程序,输入正确的密码后发现已经可以成功访问个人主页。认证授权成功!

5. 之前提到过,用户未登录时如果访问受保护资源会自动跳转到登录页面,但如果是 AJAX 请求就失效了,比如未登录的情况下点击首页的点赞时,后台会报 AccessDeniedException 异常,但是页面不跳转。我们想让用户跳转到登录页面可以这样做:

    //点赞或踩
    function upvote_click(id,cont) {
        var uid = "${user.id}";
        if(uid==''||uid==null){
            window.location.href = "/login.jsp";
            return
        }
        $.ajax({
        ...

在 upvote_click 方法中先获取用户的 id,如果用户 id 不存在则说明未登录,直接跳转到登录页面。

像校验邮箱、校验手机号等的 AJAX 我是直接放行的。

6. 用户登录成功以后,如果访问:http://localhost:8080/index.jsp 或者:http://localhost:8080 会发现页面获取不到用户信息,因为在自定义 IndexJspFilter 过滤器中没有保存用户 user,部分代码修改如下:

    public class IndexJspFilter extends BaseController implements Filter{
        ...
        User user = getCurrentUser();
        request.setAttribute("user",user);
        ...
    }

将 IndexJspFilter 继承 BaseController,然后调用它的 getCurrentUser() 方法获取当前用户信息,然后保存到 Request 域中。这样前端页面就可以通过 EL 表达式获取用户信息了。

第15课百度网盘地址:

链接:https://pan.baidu.com/s/1Bzsisjf2DtsFK5LX8_dgqg 密码:3irw

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
基于SpringSpring MVCMyBatis博客系统开发教程如下: 首先,要准备好开发所需的环境和工具。你需要安装Java开发工具包(JDK)、Eclipse或者IntelliJ IDEA集成开发环境、Apache Tomcat服务器、Maven构建工具以及MySQL数据库。确保这些工具都正确安装和配置。 接下来,创建一个新的Maven项目,并在pom.xml文件中添加依赖项,包括Spring MVCMyBatis、数据库连接池、日志等。这些依赖项可以通过Maven自动下载和管理。 然后,在src/main/java目录下创建相应的包结构,如controller、service、dao等。在dao包下创建相应的数据访问接口,并使用MyBatis提供的注解或XML配置文件实现数据访问的逻辑。在service包下创建对应的服务接口和实现类,用于处理业务逻辑。在controller包下创建控制器类,处理请求和响应。 配置SpringMyBatis的配置文件。在src/main/resources目录下创建一个名为applicationContext.xml的Spring配置文件,并添加相关的配置信息,包括数据库连接、事务管理、包扫描等。同样,在该目录下创建一个名为mybatis-config.xml的MyBatis配置文件,并配置数据源、映射文件等。 编写博客系统的前端页面和样式。可以使用HTML、CSS和JavaScript等技术来构建用户界面,使用JSP或Thymeleaf等模板引擎来动态生成页面内容。可以使用Bootstrap等前端框架来加快开发进度并提供良好的用户体验。 最后,测试和部署博客系统。使用JUnit等单元测试框架来测试各个模块的功能是否正常。将项目打包成war文件,并将其部署到Tomcat服务器上运行。 通过以上步骤,你就可以基于SpringSpring MVCMyBatis开发一个简单的博客系统了。当然,在实际开发中还会涉及到更多的细节和技术选择,需要持续学习和实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

exodus3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值