本篇来学习OAuth2和Shiro相关知识以如何在SpringBoot框架中使用它们。

OAuth2

OAuth2简介

OAuth是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等〉,而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。例如,用户想通过QQ登录知乎,这时知乎就是一个第三方应用,知乎要访问用户的一些基本信息就需要得到用户的授权,如果用户把自己的QQ用户名和密码告诉知乎,那么知乎就能访问用户的所有数据,并且只有用户修改密码才能收回授权,这种授权方式安全隐患很大,如果使用OAuth,就能很好地解决这一问题。

采用令牌的方式可以让用户灵活地对第三方应用授权或者收回权限。OAuth2是OAuth协议的下一版本,但不向下兼容OAuth1.0 。OAuth2关注客户端开发者的简易性,同时为Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的Web开发登录认证一般都是基于Session的,但是在前后端分离的架构中继续使用Session会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持Cookie(微信小程序),要么使用非常不便,对于这些问题,使用OAuth2认证都能解决。因此OAuth2对于解决信息授权与认证有非常大的帮助。

OAuth2角色

在学习OAuth2之前,了解OAuth2中的4种基本角色对于学习和理解OAuth的工作原理有重要意义。(1)资源所有者:资源所有者即用户,具有头像、照片、视频等资源;(2)客户端:客户端即第三方应用,如上方提到的知乎;(3)授权服务器:授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。(4)资源服务器:资源服务器是提供给用户资源的服务器,如头像、照片、视频等资源。

通常来说,授权服务器和资源服务器可以是同一台服务器。

OAuth2授权流程

在熟悉了OAuth2中的4个基本角色以后,接下来开始学习OAuth2的授权流程,具体的流程如下:(1)客户端(第三方应用)向用户请求授权;(2)用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端;(3)客户端拿着授权许可凭证去授权服务器申请令牌;(4)授权服务器验证信息无误后,发放令牌给客户端;(5)客户端拿着令牌去资源服务器访问资源;(6)资源服务器验证令牌无误后开放资源访问。

上述是一个大致的流程,因为OAuth2有4种不同的授权方式,每种授权模式的授权流程又存在一定的差异,不过大致流程如下图所示:

授权模式

OAuth协议的授权模式一共有4种,接下来简要介绍一下:

  • 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本上都是使用这种模式。
  • 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌,一般若网站是纯静态页面,则可以采用这种方式。
  • 密码模式:密码模式是用户把用户密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌。这需要用户对客户端高度信任,如客户端和服务提供商是同一家公司。
  • 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务器提供者申请授权。严格来说,客户端模式并不能算作OAuth协议要解决的问题的一种解决方案,但是对于开发者而言,在一些前端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是较为方便的。

这四种模式各有各自特点,分别适用于不同的开发场景,开发者需要结合使用情况进行选择。

OAuth2实践

本例要介绍的是在前后端分离应用(移动端、微信小程序等)提供的认证服务器中如何搭建OAuth服务,因此主要介绍密码模式。相应的搭建步骤如下:

第一步,创建项目和添加依赖。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为oauth2springboot,然后添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!--添加spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--web配置依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--data-redis依赖中移除Lettuce,使用Jedis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--使用Jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--添加spring oauth2依赖-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>

由于SpringBoot中的OAuth协议是在Spring Security的基础上完成的,因此首先需要添加Spring Security依赖,要使用OAuth2,因此需要先添加OAuth2相关依赖,令牌可以存储在Redis缓存服务器上,同时Redis具有过期等功能,非常适合令牌的存储,因此也需要添加Redis依赖。

项目创建成功后,接下来在在application.properties文件中添加Redis的链接信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 基本连接信息配置
# 使用Redis库的编号,Redis中提供了16个database,编号从0-15
spring.redis.database=0
# Redis实例的IP地址
spring.redis.host=192.168.2.132
# Redis端口号,默认6379
spring.redis.port=6379
# Redis登录密码
spring.redis.password=123@456

# 连接池基本信息配置
# Redis连接池的最大连接数
spring.redis.jedis.pool.max-active=8
# Redis连接池的最大空闲连接数
spring.redis.jedis.pool.max-idle=8
# Redis连接池的最大阻塞等待时间,默认为-1,表示没有限制
spring.redis.jedis.pool.max-wait=-1ms
# Redis连接池的最小空闲连接数
spring.redis.jedis.pool.min-idle=0

第二步,配置授权服务器。授权服务器和资源服务器可以是同一台服务器,也可以不是,本例子假设是同一台服务器,通过不同的配置分别开启授权服务器和资源服务器。新建config包,并在其中创建AuthorizationServerConfig类,注意这个类需要实现AuthorizationServerConfigurerAdapter类(注意使用这个类时需要指定spring-security-oauth2的版本,否则会找不到该类),里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**授权服务器**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
UserDetailsService userDetailsService;

@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);
}

/**设置客户端的信息**/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password","refresh_token")
.accessTokenValiditySeconds(1800)
.resourceIds("rid")
.scopes("all")
.secret("$2a$10$LE86rB.N5wYXAVRkLIEtGuLnPzvccwWN2/V5ZWa6g0Drk3WfvV5ou");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
}

解释一下上述代码的含义:

  • 首先自定义AuthorizationServerConfig类需要继承AuthorizationServerConfigurerAdapter类,完成对授权服务器的配置,然后通过@EnableAuthorizationServer注解开启授权服务器。
  • 接着注入AuthenticationManager对象,用来支持password模式。注入RedisConnectionFactory对象用来完成Redis缓存,将令牌信息存储到Redis缓存中。注入UserDetailsService对象,该对象将为刷新token提供技术支持。passwordEncoder方法用于返回一个加密后的密码对象,这个和前面设置一样。
  • configure(ClientDetailsServiceConfigurer clients)方法用于配置password授权模式,authorizedGrantTypes表示OAuth2中的授权模式为”password”和”refresh_token”两种。在标准的OAuth2协议中,授权模式并不包括”refresh_token”,但是在SpringSecurity的实现中将其归为一种,因此如果要实现”access_token”的刷新,就需要添加这样一种授权模式;accessTokenValiditySeconds方法设置了”access_token”的过期时间;resourceIds配置了资源id;secret方法配置了加密后的密码,明文是1234。
  • configure(AuthorizationServerEndpointsConfigurer endpoints)方法配置了令牌的存储,AuthenticationManagerUserDetailsService主要用于支持password模式以及令牌的刷新。
  • configure(AuthorizationServerSecurityConfigurer security)方法表示支持client_idclient_secret做登录认证。

第三步,配置资源服务器。在config包,内创建ResourceServerConfig类,注意这个类需要实现ResourceServerConfigurerAdapter类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**资源服务器**/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rid").stateless(true);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();
}
}

解释一下上述代码的含义:

  • 首先自定义ResourceServerConfig类需要继承ResourceServerConfigurerAdapter类,完成对资源服务器的配置,然后通过@EnableResourceServer注解开启资源服务器配置。
  • 接着使用resources.resourceId("rid").stateless(true);来配置资源id,这里的资源id和授权服务器中的资源id需要保持一致,然后设置这些资源仅基于令牌认证。之后就是配置HttpSecurity,里面定义一些URL规则等,这个和前面的配置几乎一致,因此也就不再赘述了。

第四步,配置Security。新建WebSecurityConfig类,注意它需要继承WebSecurityConfigurerAdapter类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**Spring Security相关配置**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**内存验证方式**/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$LE86rB.N5wYXAVRkLIEtGuLnPzvccwWN2/V5ZWa6g0Drk3WfvV5ou")
.roles("admin")
.and()
.withUser("envy")
.password("$2a$10$LE86rB.N5wYXAVRkLIEtGuLnPzvccwWN2/V5ZWa6g0Drk3WfvV5ou")
.roles("user");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Bean
@Override
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**").authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}

可以发现这里SpringSecurity的配置和前面的配置基本上一致,只是多了两个Bean,这两个Bean将注入授权服务器配置类中使用。另外这里的HttpSecurity配置主要是配置/oauth/**模式的URL,这一类的请求直接放行。在SpringSecurity配置和资源服务器配置中,一共涉及到两个HttpSecurity,其中SpringSecurity中的配置优先级高于资源服务器中的配置,即请求地址先经过SpringSecurity中的HttpSecurity,再经过资源服务器的HttpSecurity

第五步,验证测试。新建HelloController类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin(){
return "hello,admin!";
}

@GetMapping("/user/hello")
public String user(){
return "hello,user!";
}

@GetMapping("/hello")
public String hello(){
return "hello!";
}
}

根据前面的配置可知,想成功访问这三个链接需要admin角色、user角色和登录后访问。

当所有的配置都完成以后,先启动Redis服务器,然后再启动SpringBoot项目。首先发送一个POST请求,用于获取token,请求地址如下(注意这是一个POST请求,为了显示方便将参数写在地址栏中):

1
http://localhost:8080/oauth/token?username=envy&password=1234&grant_type=password&client_id=password&scope=all&client_secret=1234

可以看到该请求地址中包含了用户名、密码、授权模式、客户端id、scope以及客户端密码,这些基本上就是授权服务器内配置的数据。当该请求访问成功后,页面返回一段JOSN信息:

而这个返回结果就包含了5项信息:access_token、token_type、refresh_token、expires_in和scope。其中access_token是获取其他资源时需要使用到的令牌,token_type是令牌类型,refresh_token用来刷新令牌,expires_in表示access_token的过期时间,当access_token过期后,使用refresh_token重新获取新的access_token(前提是refresh_token未过期),请求地址如下(注意这里也是POST请求):

1
http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=1710f670-4843-4886-bc81-5df9500d73b2&client_id=password&client_secret=1234

注意到获取新的access_token时需要携带refresh_token,同时授权模式设置为refresh_token,在获取的结果中access_token也会变化,同时access_token的有效期也会变化,如下图所示:

接下来访问所有的资源时,只需要携带access_token参数即可,如/user/hello接口:

1
http://localhost:8080/user/hello?access_token=1df2dc6f-51e0-44c1-9c64-2546990617c1

访问结果如下图所示:

如果非法访问一个资源,如envy用户访问/admin/hello接口时,页面就会返回一个无法请求的信息提示:

现在来查看一下redis的数据:

这样一个password模式的OAuth认证体系就搭建完成了。

OAuth中的认证模式有4种,开发者需要结合实际情况来选择其中的一种,本例只是介绍了在前后端分离应用中常见的password模式,其他的授权模式也都有自己的使用场景,这些笔者都会在后续文章中依次进行介绍。

其实通过前一篇和本篇到目前的学习,开发者可能也意识到了Spring Security OAuth2的使用还是较复杂的,配置也比较繁琐,如果开发者的使用场景比较简单,完全可以按照前面介绍的授权流程来搭建属于自己的OAuth2认证体系。如果你之前使用过微信支付,可以发现这个流程其实非常相似,且获取用户信息的方式也很类似这里就不介绍了,有兴趣的可以自行去学习。

Shiro

Shiro简介

Apache Shiro是一个开源的轻量级的Java安全框架,它提供身份验证、授权、密码管理以及会话管理等功能。相对于Spring Security, Shiro框架更加直观、易用,同时也能提供健壮的安全性。在传统的SSM框架中,手动整合Shiro时需要较为繁琐的配置步骤。而针对SpringBoot,Shiro官方提供了shiro-spring-boot-web-starter来简化Shiro在SpringBoot中的配置。接下来介绍
shiro-spring-boot-web-starter的使用步骤。

SpringBoot整合Shiro框架

第一步,创建SpringBoot Web项目并添加依赖。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为shirospringboot,然后在pom.xml文件中添加Shiro依赖以及页面模板引擎依赖,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--添加Shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!--添加thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--添加thymeleaf-extras-shiro依赖-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

特别注意这里不需要添加spring-boot-starter-web依赖,因为shiro-spring-boot-web-starter中已经依赖了spring-boot-starter-web依赖。同时本案例使用了Thymeleaf模板,因此需要添加Thymeleaf依赖,另外为了在Thymeleaf中使用shiro标签,因此需要引入thymeleaf-extras-shiro依赖。
第二步,Shiro基本配置。首先在application.properties配置文件中配置Shiro的基本信息,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开启Shiro配置,默认为true
shiro.enabled=true
# 开启Shiro Web配置,默认为true
shiro.web.enabled=true
# 设置登录地址,默认为/login.jsp
shiro.loginUrl=/login
# 设置登录成功地址,默认为/
shiro.successUrl=/index
# 设置未获授权默认跳转地址
shiro.unauthorizedUrl=/unauthorized
# 是否允许通过URL参数实现会话跟踪,如果网站支持Cookie,可以关闭该选项,默认为true
shiro.sessionManager.sessionIdUrlRewritingEnabled=true
# 是否允许通过Cookie实现会话跟踪,默认为true
shiro.sessionManager.sessionIdCookieEnabled=true

Shiro基本信息配置完成后,接下来在Java代码中配置Shiro,只需提供两个最基本的Bean即可。新建config包,并在其中创建ShiroConfig类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class ShiroConfig {
@Bean
public Realm realm(){
TextConfigurationRealm realm = new TextConfigurationRealm();
realm.setUserDefinitions("envy=1234,user\n admin=1234,admin");
realm.setRoleDefinitions("admin=read,write\n user=read");
return realm;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login","anon");
chainDefinition.addPathDefinition("/doLogin","anon");
chainDefinition.addPathDefinition("/logout","logout");
chainDefinition.addPathDefinition("/**","authc");
return chainDefinition;
}
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}

解释一下上述代码的含义:

  • 这里提供了两个关键Bean,一个所示Realm,另一个是ShiroFilterChainDefinition。至于ShiroDialect则是为了支持在Thymeleaf中使用Shiro标签,如果不在Thymeleaf中使用Shiro标签,那么可以不提供ShiroDialect
  • Realm可以是自定义的Realm,也可以是Shiro提供的Realm,简单起见这里没有配置数据库连接,这里直接配置了两个用户:envy/1234和admin/1234,分别对应角色user和admin,其中user角色只具有read权限,而admin角色拥有read和write权限。
  • ShiroFilterChainDefinition方法中配置了基本的过滤规则,/login/doLogin可以匿名访问,/logout是一个注销登录的请求,其余的请求都需要认证后才能访问。

第三步,新建controller类。接下来就是配置登录接口以及页面访问接口。新建一个controller包,并在其中创建UserController类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Controller
public class UserController {

@PostMapping("/doLogin")
public String doLogin(String username, String password, Model model){
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
}catch (AuthorizationException e){
model.addAttribute("error","用户名或密码输入错误!");
return "login";
}
return "redirect:/index";
}

@RequiresRoles("admin")
@GetMapping("/admin")
public String admin(){
return "admin";
}

@RequiresRoles(value = {"admin","user"},logical = Logical.OR)
@GetMapping("/user")
public String user(){
return "user";
}
}

简单解释一下上述代码的含义:

  • doLogin方法中,首先构造一个UsernamePasswordToken的实例,然后获取到一个Subject对象,并调用该对象中的login方法执行登录操作,在登录操作执行过程中,当有异常抛出时,说明登录失败,页面需要携带信息并返回给登录视图;当登录成功时,则重定向到/index接口。
  • 接下来暴露两个接口/admin/user,对于/admin接口来说需要具有admin角色的用户才能访问;而对于/user接口而言,具备admin或者user角色中的任意一个即可访问,因此需要使用Logical.OR来表示这种逻辑或关系。
  • 请注意由于这里是使用模板引擎,因此需要使用@Controller注解,而不是@RestController注解,这一点需要注意。

对于其他不需要角色就能访问的接口,直接定义在WebMvc中是最佳选择。新建一个WebMvcConfig类,注意它需要实现WebMvcConfigurer接口,并重写其中的addViewControllers方法,其中的代码为:

1
2
3
4
5
6
7
8
9
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/index").setViewName("index");
registry.addViewController("/unauthorized").setViewName("unauthorized");
}
}

这里就设置了三个URL,login、index和authorized页面及视图名称,访问这些URL是不需要经过controller控制器的。
第四步,新建异常处理类。接着创建全局异常处理器进行全局异常处理,本例子主要是处理授权异常。新建一个ExceptionController类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(AuthorizationException.class)
public ModelAndView error(AuthorizationException e){
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("error",e.getMessage());
modelAndView.setViewName("unauthorized");
return modelAndView;
}
}

当用户访问位授权的资源时,会自动跳转到unauthorized视图,并携带相应的出错信息。
第五步,新建对应的模板页面。当上述信息均配置完成时,接下来在resources/templates目录下创建5个HTML页面用于后续测试。
(1)新建index.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h3>Hello,<shiro:principal/></h3>
<h3><a href="/logout">注销登录</a></h3>
<h3><a shiro:hasRole="admin" href="/admin">管理员页面</a></h3>
<h3><a shiro:hasAllRoles="admin,user" href="/user">普通用户页面</a></h3>
</body>
</html>

index.html是登录成功后的首页,首先展示当前登录用户的用户名,然后展示一个“注销登录”链接,若当前登录用户具备“admin”角色,则展示一个“管理员页面”的超链接;若当前登录用户具备“admin”或者“user”角色,则展示一个“普通用户页面”的超链接。注意这里导入的名称空间是xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"和在JSP页面中导入的Shiro名称空间不一致。

(2)新建login.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<div th:text="${error}"></div>
<input type="submit" value="登录">
</form>
</body>
</html>

login.html是一个普通的登录页面,在登录失败时通过一个div来显示登录失败的信息。
(3)新建user.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>普通用户</title>
</head>
<body>
<h1>普通用户个人页面</h1>
</body>
</html>

user.html是一个普通的用户信息展示页面。
(4)新建admin.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>管理员用户</title>
</head>
<body>
<h1>管理员个人页面</h1>
</body>
</html>

admin.html是一个管理员的信息展示页面。
(5)新建unauthorized.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>非法访问</title>
</head>
<body>
<h3>对不起,未获授权,非法访问</h3>
<h3 th:text="${error}"></h3>
</body>
</html>

unauthorized.html是一个授权失败的信息展示页面,该页面还会展示授权出错的信息。

第六步,测试。当上述信息均配置完成时,启动SpringBoot项目,访问登录页面,分别使用envy/1234和admin/1234进行登录,结果如下图所示:

注意因为envy用户不具备admin角色,因此登录成功后的页面是上没有前往管理员页面的超链接。

登录成功后,无论是envy还是admin用户,单机“注销登录”都会注销成功,然后回到登录页面,envy用户因为不具备admin角色,因此登录成功后的页面是上没有前往管理员页面的超链接,无法进入到管理员页面中。此时若开发者使用envy用户登录,然后手动在浏览器地址栏中输入http://localhost:8080/admin,则页面会跳转到未授权页面,如下图所示:

以上通过一个简单的例子学习了如何在SpringBoot中整合Shiro以及如何在Thymeleaf中使用Shiro标签,一旦整合成功,接下来Shiro的用法就和原来的一模一样。注意这里仅仅是介绍了SpringBoot整合Shiro,对于Shiro的其他用法这里未介绍,后续会出一些文章来介绍关于Shiro的用法。

安全管理小结

在第十四篇中学习了如何在SpringBoot中整合SpringSecurity,而在第十五篇中学习了SpringBoot中整合Shiro。对于SpringSecurity,有基于传统认证方式的Session认证,也有使用OAuth2的认证。一般来说,在传统的Web架构中,使用Session认证方便快速,但是如果结合微服务、前后端分离等架构,则使用OAuth认证更加方便,具体使用哪一种,需要开发者根据实际情况进行选择。对于Shiro来说,虽然功能不及SpringSecurity强大,但是简单易用,而且也能胜任大部分的中小型项目。当然,在SpringBoot 项目中, Spring Security的整合显然要更加容易,因此可以首选Spring Security。如果开发团队对Spring Security不熟悉,但是熟悉Shiro的使用,当然也可以使用Shiro,这个要结合具体情况来定。但是无论是使用哪种框架,都是为了保证项目的安全,这一点需要引起高度重视。