序言

后面咱们学习了如下内容:

5 分钟入门 shiro 平安框架实战笔记

shiro 整合 spring 实战及源码详解

置信大家对于 shiro 曾经有了最根本的意识,这一节咱们一起来学习写如何将 shiro 与 springmvc 进行整合。

spring mvc 整合源码

maven 依赖

  • 版本号
<properties>    <jetty.version>9.4.34.v20201102</jetty.version>    <shiro.version>1.7.0</shiro.version>    <spring.version>5.2.8.RELEASE</spring.version>    <taglibs.standard.version>1.2.5</taglibs.standard.version></properties>
  • shiro 相干依赖
<dependency>    <groupId>org.apache.shiro</groupId>    <artifactId>shiro-core</artifactId>    <version>${shiro.version}</version></dependency><dependency>    <groupId>org.apache.shiro</groupId>    <artifactId>shiro-spring</artifactId>    <version>${shiro.version}</version></dependency><dependency>    <groupId>org.apache.shiro</groupId>    <artifactId>shiro-web</artifactId>    <version>${shiro.version}</version></dependency>
  • 其余依赖

次要是 servlet、spring、数据库和 tags

<dependency>    <groupId>javax.annotation</groupId>    <artifactId>javax.annotation-api</artifactId>    <version>1.3.2</version></dependency><dependency>    <groupId>javax.servlet</groupId>    <artifactId>javax.servlet-api</artifactId>    <version>3.1.0</version>    <scope>provided</scope></dependency><dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-context</artifactId>    <version>${spring.version}</version></dependency><dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-jdbc</artifactId>    <version>${spring.version}</version></dependency><dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-webmvc</artifactId>    <version>${spring.version}</version></dependency><dependency>    <groupId>org.hsqldb</groupId>    <artifactId>hsqldb</artifactId>    <version>2.5.0</version>    <scope>runtime</scope></dependency><dependency>    <groupId>org.apache.taglibs</groupId>    <artifactId>taglibs-standard-spec</artifactId>    <version>${taglibs.standard.version}</version>    <scope>runtime</scope></dependency><dependency>    <groupId>org.apache.taglibs</groupId>    <artifactId>taglibs-standard-impl</artifactId>    <version>${taglibs.standard.version}</version>    <scope>runtime</scope></dependency>
  • jetty

依赖于 jetty 作为容器启动:

<plugin>    <groupId>org.eclipse.jetty</groupId>    <artifactId>jetty-maven-plugin</artifactId>    <version>${jetty.version}</version>    <configuration>        <httpConnector>            <port>8080</port>        </httpConnector>        <webApp>            <contextPath>/</contextPath>        </webApp>    </configuration></plugin>

配置

  • applicaiton.properties

次要指定了 shiro 相干的配置

# Let Shiro Manage the sessionsshiro.userNativeSessionManager = true# disable URL session rewritingshiro.sessionManager.sessionIdUrlRewritingEnabled = false# 登录地址shiro.loginUrl = /s/login# 登录胜利shiro.successUrl = /s/index# 未受权shiro.unauthorizedUrl = /s/unauthorized

LoginController 登录控制器

咱们首先来看一下后端的登录控制器:

import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.UsernamePasswordToken;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;/** * Spring MVC controller responsible for authenticating the user. * * @since 0.1 */@Component@RequestMapping("/s/login")public class LoginController {    private static transient final Logger log = LoggerFactory.getLogger(LoginController.class);    private static String loginView = "login";    @RequestMapping(method = RequestMethod.GET)    protected String view() {        return loginView;    }    @RequestMapping(method = RequestMethod.POST)    protected String onSubmit(@RequestParam("username") String username,                              @RequestParam("password") String password,                              Model model) throws Exception {        UsernamePasswordToken token = new UsernamePasswordToken(username, password);        try {            SecurityUtils.getSubject().login(token);        } catch (AuthenticationException e) {            log.debug("Error authenticating.", e);            model.addAttribute("errorInvalidLogin", "The username or password was not correct.");            return loginView;        }        return "redirect:/s/index";    }}

登录的校验非常简单,间接依据页面的账户明码,而后执行登录校验。

LogoutController 登出控制器

登出间接调用对应的 logout 办法,并且重定向到登录页面。

import org.apache.shiro.SecurityUtils;import org.springframework.stereotype.Component;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.mvc.AbstractController;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/** * Controller responsible for logging out the current user by invoking * {@link org.apache.shiro.subject.Subject#logout()} * * @since 0.1 */@Component@RequestMapping("/s/logout")public class LogoutController extends AbstractController {    @RequestMapping(method = RequestMethod.GET)    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {        SecurityUtils.getSubject().logout();        return new ModelAndView("redirect:login");    }}

外围组件

当然,下面的实现看起来非常简单。

数据筹备

实际上还有一些用户的账户明码信息筹备,是间接通过 BootstrapDataPopulator 类实现的,将账户信息存储到内存数据库 hsqldb 中。

SaltAwareJdbcRealm

针对畛域信息的获取实现如下:

import org.apache.shiro.authc.*;import org.apache.shiro.realm.jdbc.JdbcRealm;import org.apache.shiro.util.ByteSource;import org.apache.shiro.util.JdbcUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;/** * Realm that exists to support salted credentials.  The JdbcRealm implementation needs to be updated in a future * Shiro release to handle this. */public class SaltAwareJdbcRealm extends JdbcRealm {    private static final Logger log = LoggerFactory.getLogger(SaltAwareJdbcRealm.class);    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {        UsernamePasswordToken upToken = (UsernamePasswordToken) token;        String username = upToken.getUsername();        // Null username is invalid        if (username == null) {            throw new AccountException("Null usernames are not allowed by this realm.");        }        Connection conn = null;        AuthenticationInfo info = null;        try {            conn = dataSource.getConnection();            String password = getPasswordForUser(conn, username);            if (password == null) {                throw new UnknownAccountException("No account found for user [" + username + "]");            }            SimpleAuthenticationInfo saInfo = new SimpleAuthenticationInfo(username, password, getName());            /**             * This (very bad) example uses the username as the salt in this sample app.  DON'T DO THIS IN A REAL APP!             *             * Salts should not be based on anything that a user could enter (attackers can exploit this).  Instead             * they should ideally be cryptographically-strong randomly generated numbers.             */            saInfo.setCredentialsSalt(ByteSource.Util.bytes(username));            info = saInfo;        } catch (SQLException e) {            final String message = "There was a SQL error while authenticating user [" + username + "]";            if (log.isErrorEnabled()) {                log.error(message, e);            }            // Rethrow any SQL errors as an authentication exception            throw new AuthenticationException(message, e);        } finally {            JdbcUtils.closeConnection(conn);        }        return info;    }    private String getPasswordForUser(Connection conn, String username) throws SQLException {        PreparedStatement ps = null;        ResultSet rs = null;        String password = null;        try {            ps = conn.prepareStatement(authenticationQuery);            ps.setString(1, username);            // Execute query            rs = ps.executeQuery();            // Loop over results - although we are only expecting one result, since usernames should be unique            boolean foundResult = false;            while (rs.next()) {                // Check to ensure only one row is processed                if (foundResult) {                    throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");                }                password = rs.getString(1);                foundResult = true;            }        } finally {            JdbcUtils.closeResultSet(rs);            JdbcUtils.closeStatement(ps);        }        return password;    }}

这里间接通过默认的 sql

select password from users where username = ?

获取账户信息,而后进行最简略的加密验证。

web.xml 配置

仔细的小伙伴兴许发现了,这个 mvc 我的项目中没有 web.xml 文件。

那么,个别须要指定的配置是如何指定的呢?

官网给出的案例有另外一个配置类实现了这个性能。

import org.springframework.web.WebApplicationInitializer;import org.springframework.web.context.ContextLoaderListener;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import org.springframework.web.filter.DelegatingFilterProxy;import org.springframework.web.servlet.DispatcherServlet;import javax.servlet.DispatcherType;import javax.servlet.FilterRegistration;import javax.servlet.ServletContext;import javax.servlet.ServletRegistration;import java.util.EnumSet;/** * Initializes Spring Environment without the need for a web.xml */public class ServletApplicationInitializer implements WebApplicationInitializer {    @Override    public void onStartup(ServletContext container) {        //now add the annotations        AnnotationConfigWebApplicationContext appContext = getContext();        // Manage the lifecycle of the root application context        container.addListener(new ContextLoaderListener(appContext));        FilterRegistration.Dynamic shiroFilter = container.addFilter("shiroFilterFactoryBean", DelegatingFilterProxy.class);        shiroFilter.setInitParameter("targetFilterLifecycle", "true");        shiroFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "/*");        ServletRegistration.Dynamic remotingDispatcher = container.addServlet("remoting", new DispatcherServlet(appContext));        remotingDispatcher.setLoadOnStartup(1);        remotingDispatcher.addMapping("/remoting/*");        ServletRegistration.Dynamic dispatcher = container.addServlet("DispatcherServlet", new DispatcherServlet(appContext));        dispatcher.setLoadOnStartup(1);        dispatcher.addMapping("/");    }    private AnnotationConfigWebApplicationContext getContext() {        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();        context.setConfigLocation(getClass().getPackage().getName());        return context;    }}

受权办法

当然,不同的用户登录的权限不同,必定是因为咱们定义了不同的权限。

import org.apache.shiro.authz.annotation.RequiresPermissions;import org.apache.shiro.authz.annotation.RequiresRoles;/** * Business manager interface used for sample application. * * @since 0.1 */public interface SampleManager {    /**     * Method that requires <tt>role1</tt> in order to be invoked.     */    @RequiresRoles("role1")    void secureMethod1();    /**     * Method that requires <tt>role2</tt> in order to be invoked.     */    @RequiresRoles("role2")    void secureMethod2();    /**     * Method that requires <tt>permission1</tt> in order to be invoked.     */    @RequiresPermissions("permission2")    void secureMethod3();}

这里通过 @RequiresRoles@RequiresPermissions 指定了办法拜访须要的角色或者权限。

实战成果

为了便于大家学习,上述代码曾经全副开源:

https://github.com/houbb/shiro-inaction/tree/master/shiro-inaction-02-springmvc

登录页面

启动程序,浏览器间接拜访 http://localhost:8080/,会被重定向到登录页面。

user1 登录

咱们应用 user1 登录:

user2 登录

咱们应用 user2 登录:

登出

间接点击页面的登出链接,就能够实现登出。

实现原理

思考

当初,老马和大家一起思考一个问题。

咱们在 application.properties 文件中指定了对应的登录/登出门路,那么 shiro 是如何映射并且执行的呢?

答案就是 Filter。

针对每一个申请,shiro 会判断申请的 url 是否和咱们指定的 url 匹配,并且调用对应的 filter,而后登程对应的办法。

实际上 shiro 中有很多内置的 filter 实现,咱们选取其中的几个做下介绍。

登录验证 Filter

匿名

最简略的就是所有的用户都能够拜访,实现也最简略:

import org.apache.shiro.web.filter.PathMatchingFilter;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse; * @since 0.9 */public class AnonymousFilter extends PathMatchingFilter {    /**     * Always returns <code>true</code> allowing unchecked access to the underlying path or resource.     *     * @return <code>true</code> always, allowing unchecked access to the underlying path or resource.     */    @Override    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) {        // Always return true since we allow access to anyone        return true;    }}

这种适宜登录页面之类的,比方能够指定如下

/user/signup/** = anon

form 表单提交

还有比拟罕用的就是 form 表单提交,springboot 整合的时候甚至能够省略掉咱们写的登录校验实现。

/** * <p>If you would prefer to handle the authentication validation and login in your own code, consider using the * {@link PassThruAuthenticationFilter} instead, which allows requests to the * {@link #loginUrl} to pass through to your application's code directly. * * @see PassThruAuthenticationFilter * @since 0.9 */public class FormAuthenticationFilter extends AuthenticatingFilter {    //TODO - complete JavaDoc    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = "shiroLoginFailure";    public static final String DEFAULT_USERNAME_PARAM = "username";    public static final String DEFAULT_PASSWORD_PARAM = "password";    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";    private static final Logger log = LoggerFactory.getLogger(FormAuthenticationFilter.class);    private String usernameParam = DEFAULT_USERNAME_PARAM;    private String passwordParam = DEFAULT_PASSWORD_PARAM;    private String rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;    private String failureKeyAttribute = DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;    public FormAuthenticationFilter() {        setLoginUrl(DEFAULT_LOGIN_URL);    }    @Override    public void setLoginUrl(String loginUrl) {        String previous = getLoginUrl();        if (previous != null) {            this.appliedPaths.remove(previous);        }        super.setLoginUrl(loginUrl);        if (log.isTraceEnabled()) {            log.trace("Adding login url to applied paths.");        }        this.appliedPaths.put(getLoginUrl(), null);    }    //...}

当然能够有很多种形式,次要就是构建出登录的账户明码信息。

这里继承自 AuthenticatingFilter 实现类,会调用对应的登录办法:

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {    AuthenticationToken token = createToken(request, response);    if (token == null) {        String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +                "must be created in order to execute a login attempt.";        throw new IllegalStateException(msg);    }    try {        Subject subject = getSubject(request, response);        subject.login(token);        return onLoginSuccess(token, subject, request, response);    } catch (AuthenticationException e) {        return onLoginFailure(token, e, request, response);    }}

登出验证 Filter

shiro 也为咱们实现了内置的登出过滤器。

/** * Simple Filter that, upon receiving a request, will immediately log-out the currently executing * {@link #getSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) subject} * and then redirect them to a configured {@link #getRedirectUrl() redirectUrl}. * * @since 1.2 */public class LogoutFilter extends AdviceFilter {        //...    /**     * Acquires the currently executing {@link #getSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) subject},     * a potentially Subject or request-specific     * {@link #getRedirectUrl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, org.apache.shiro.subject.Subject) redirectUrl},     * and redirects the end-user to that redirect url.     *     * @param request  the incoming ServletRequest     * @param response the outgoing ServletResponse     * @return {@code false} always as typically no further interaction should be done after user logout.     * @throws Exception if there is any error.     */    @Override    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {        // 获取主题信息        Subject subject = getSubject(request, response);        // 检测是否只反对 POST 形式登出        // Check if POST only logout is enabled        if (isPostOnlyLogout()) {            // check if the current request's method is a POST, if not redirect            if (!WebUtils.toHttp(request).getMethod().toUpperCase(Locale.ENGLISH).equals("POST")) {                // 返回对应的非 post 登出的响应               return onLogoutRequestNotAPost(request, response);            }        }        // 获取重定向的地址        String redirectUrl = getRedirectUrl(request, response, subject);        //try/catch added for SHIRO-298:        try {            // 执行登出办法            subject.logout();        } catch (SessionException ise) {            log.debug("Encountered session exception during logout.  This can generally safely be ignored.", ise);        }        issueRedirect(request, response, redirectUrl);        return false;    }    //...}

受权验证 Filter

RolesAuthorizationFilter 角色受权过滤器

import java.io.IOException;import java.util.Set;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import org.apache.shiro.subject.Subject;import org.apache.shiro.util.CollectionUtils;/** * Filter that allows access if the current user has the roles specified by the mapped value, or denies access * if the user does not have all of the roles specified. * * @since 0.9 */public class RolesAuthorizationFilter extends AuthorizationFilter {    //TODO - complete JavaDoc    @SuppressWarnings({"unchecked"})    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {        // 获取以后主题        Subject subject = getSubject(request, response);        // 获取须要的角色列表        String[] rolesArray = (String[]) mappedValue;        if (rolesArray == null || rolesArray.length == 0) {            //no roles specified, so nothing to check - allow access.            return true;        }        // 判断是否领有指定的角色        Set<String> roles = CollectionUtils.asSet(rolesArray);        return subject.hasAllRoles(roles);    }}

PermissionsAuthorizationFilter 权限受权过滤器

import java.io.IOException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import org.apache.shiro.subject.Subject;/** * Filter that allows access if the current user has the permissions specified by the mapped value, or denies access * if the user does not have all of the permissions specified. * * @since 0.9 */public class PermissionsAuthorizationFilter extends AuthorizationFilter {    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {        // 获取主题        Subject subject = getSubject(request, response);        // 须要的权限        String[] perms = (String[]) mappedValue;        boolean isPermitted = true;        if (perms != null && perms.length > 0) {            if (perms.length == 1) {                // 如果列表长度为1,进行校验                if (!subject.isPermitted(perms[0])) {                    isPermitted = false;                }            } else {                // 如果须要多个,执行校验                if (!subject.isPermittedAll(perms)) {                    isPermitted = false;                }            }        }        return isPermitted;    }}

小结

这一节咱们解说了如何整合 springmvc 与 shiro,能够发现 shiro 内置了十分多的实现,帮忙咱们简化登录的设计实现。

不过应用过 springboot 的小伙伴都晓得,咱们的实现能够变得更加简化。

能够浏览 springboot 与 shiro 的整合:

shiro 整合 springboot 实战笔记

心愿本文对你有所帮忙,如果喜爱,欢送点赞珍藏转发一波。

我是老马,期待与你的下次相遇。

参考资料

10 Minute Tutorial on Apache Shiro

https://shiro.apache.org/reference.html

https://shiro.apache.org/session-management.html

本文由博客一文多发平台 OpenWrite 公布!