Javaweb——Spring Boot 系列(16)认证与授权

  • 为了系统具有一定的安全性,也为了将普通用户和管理员用户区分开来,大多数企业级的 Web 都配备了安全机制,用户认证与授权就是一种。

一、Spring Boot 对认证与授权机制的支持

  • 作为优秀的框架 Spring 或者说 Spring Boot 想当然的对用户认证与授权机制进行了支持,Spring 框架提供了一个 Spring Security 组件为 Spring 项目提供一个安全框架,利用依赖注入和 AOP 实现安全相关的功能。
  • 作为安全框架,认证(Authentication)和认证(Authorization)是整个框架中相当重要的概念,认证确定用户是否可以登录系统,授权确定用户可以使用哪些系统功能。

1、Spring Security 的如何配置

  • Spring Security 主要通过过滤器来过滤访问请求从而达到安全的功能,在不使用 Spring Boot 的情况下 Spring Security 的配置,需要注册一个特殊的 DelegatingFilterProxy 过滤器到 WebApplicationInitializer;或者在自己的 Initializer 类继承 AbstractSecurityWebApplicationInitializer 抽象类。
  • 通常在一个配置类上用注解 @EnableWebSecurity 并继承 WebSecurityConfigurerAdapter 便能达到对 Spring Security 的配置,具体的安全配置则通过重写 config 方法实现。
  • 而在 Spring Boot 中的话,已经为我们准备了对 Spring Security 的自动配置:SecurityAutoConfiguration 类和 SecurityProperties 类。前者主要有如下的自动配置:
    1)自动配置一个内存中的用户,账号为 user,密码在项目启动时在控制台窗口可以看到,相当复杂的一串字符;
    2)忽略 /css/、/js/、/iamges/ 和 /favicon.ico 等静态文件的拦截。
    3)自动配置 securityFilterChainRegistration 的 Bean。
  • 而 SecurityProperties 提供在 application.properties 文件中以 “security” 为前缀的相关配置,主要如下:
    securtiy.user.name=user # 内存中的默认用户账户
    securtiy.user.password= # 用户密码
    securtiy.user.role = USER # 用户角色,默认是 USER
    securtiy.require-ssl = false # 是否需要 SSL 支持,默认不需要
    securtiy.enable-csrf=false # 是否开启“跨站请求伪造” 支持。默认关闭
    securtiy.basic.enabled = true
    securtiy.basic.path= # /**
    securtiy.basic.authorize -mode=
    securtiy.filter-order=0
    securtiy.headers.xss=false
    securtiy.headers.cache=false
    securtiy.headers.frame=false
    securtiy.headers.content-type=false
    security.headers.hsts=all
    security.sessions=stateless
    security.igonre= # 用逗号隔开无需拦截的路径
    
  • 因为 Spring Boot 为我们做了大量自动配置,因此只需要在配置类继承 WebSecurityConfigurerAdapter 类即可。

2、进行认证的用户

  • 认证需要有一套用户资源,授权就是对某个用户赋予相应的角色权限,Spring Security 通过重写 config(AuthenticationManagerBuilder auth) 方法实现定制。
  • 用户可以是内存中的用户,也可以是 JDBC 中的用户,前者用 AuthenticationManagerBuilder 的 inMemoryAuthentication 方法添加并指定用户权限即可,示例:
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    	auth.inMemoryAuthentication
    	.withUser("ts").password("ts").roles("ROLE_ADMIN")
    	.and()
    	.withUser("demo").password("demo").roles("ROLE_USER");
    }
    
  • 而 对于 JDBC 中的用户,直接指定 DataSource 即可,如下:
    @Autowired
    DataSource dataSource;
    @Override
    protected void configure(AuthenicationManagerBuild auth) throws Exception{
    	auth.jdbcAuthentication().dataSource(dataSource);
    }
    
  • 当然用户来源也可以是其他的,此时可以自定义实现 UserDetailsService 接口,因为上面的两种其实就是对该接口的一种实现,自定义示例如下:
    public class CustomUserService implements UserDetailsService{
    	@Autowired
    	customRepository repository;
    
    	@Override
    	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    		customUser user = repository.findByUserName(username);
    		List<GrantedAuthority> auth = new ArrayList<>();
    		auth.add(new SimpleGrantedAuthority("ROLE_ADMIN");
    		return new User(user.getUsername(),user.getPassword(),auth);
    	}
    }
    
  • 然后再对这个 CustomUserService 进行注册,示例:
    @Bean
    UserDetailsService customUserService(){
    	return new CustomUserService();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    	auth.userDetailsService(customUserService());
    }
    

3、如何进行授权

  • Spring Security 通过拦截器对请求进行过滤和拦截,从而起到一种安全措施,只用被授权允许访问的用户才不会被拦截。
  • 实现请求拦截通过重写 configure(HttpSecurity http) 方法。Spring Security 有两种规则来对请求路径进行匹配:antMatchers 和 regesMatchers;前者用 Ant 风格的路径匹配,后者用正则表达式匹配路径。
  • 在路径匹配之后,是对请求访问的用户,针对其用户信息对请求路径进行安全处理,相关方法如下:
    方法 作用
    access(String) Spring EL 表达式结果为 true 时可访问
    anonymous() 匿名可访问
    denyAll() 用户不能访问
    fullyAuthenticated() 用户完全认证可访问(非 remember 下自动登录)
    hasAnyAuthority(String…) 如果用户有参数,则其中任一权限可访问
    hasAnyRole(String…) 如果用户有参数,则其中任一角色可访问
    hasAuthority(String) 如果用户有参数,则其权限可访问
    hasIpAddress(String) 如果用户来自参数中的 IP 可访问
    hasRole(String) 用户若有参数中的角色可以访问
    permitAll() 用户可以任意访问
    rememberMe() 允许通过 remember-me 登录的用户访问
    authenticated() 用户登录后可访问
  • 授权示例:
    @Override
    protected void configure(HttpSecurity http) throws Exception{
    	http.authorizeRequests()	// 开始请求权限配置
    	.antMatchers("/admin/**").hasRole("ROLE_ADMIN) //只有拥有 ROLE_ADMIN 角色的用户才可以访问路径 /admin/**
    	.antMatchers("/user/**").hasAnyRole("ROLE_ADMIN","ROLE_USER")
    	.anyRequest().authenticated(); //其余所有用户需要登录认证才能访问
    }
    

4、登陆行为定制

  • Spring Security 支持对登录行为进行定制,如下:
    @Override
    protected void configure(HttpSecurity http) throws Exception{
    	http
    		.formLogin()  //开始登录制作
    		.loginPage("/login")	//登录页面
    		.defaultSuccessUrl("/index")	//登录成功后跳转的页面
    		.failureUrl("/login?error")	//登录失败后跳转的页面
    		.permitAll()
    		.and()
    		.rememberMe()	//开启 Cookie 存储用户信息
    		.tokenVailditySeconds(1209600)	//指定 cookie 的有效期为1209600秒,即两个星期
    		.key("myKey")	// cookie 中的私钥
    		.and()
    		.logout()	//定制注销行为
    		.logoutUrl("/custom-logout")	//指定注销的 URL 路径
    		.logoutSuccessUrl("/logout-success")	//注销成功后跳转页面
    		.permitAll();
    }
    

三、完整项目示例

  • 接下来用一个完整的项目演示登录与注销行为,以及对不同登录用户进行认证和授权行为。

1、新建项目

  • 新建一个 Spring Boot 项目,因为演示页面的原因需要 Thymeleaf 的支持,因此初始依赖选择 JPA、Security 和 Thymeleaf,目标数据库为 Oracle,因此需要 ojdbc6.jar 这个驱动包,同时为了 Thymeleaf 页面支持 Spring Security 需要添加依赖,因此需要手动添加如下两个依赖:
    <!--        Oracle 数据库驱动-->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.2.0</version>
        </dependency>
    <!--        Thymeleaf 的 Spring Security 支持-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>
    

2、项目配置

  • 在 application.properties 对数据源和日志等进行配置,如下:
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
    spring.datasource.url=jdbc\:oracle\:thin\:@localhost\:1521\:xe
    spring.datasource.username=boot
    spring.datasource.password=boot
    
    logging.file=log.log
    logging.level.org.springframework.security=INFO
    
    spring.thymeleaf.cache=false
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.show-sql=true
    

3、静态资源

  • 为了页面稍微美观,又不想自己写那么多 CSS,所以直接用先成的前端框架 Bootstrap,为了避免静态资源被错误拦截,因此放在指定位置,CSS 文件默认放置在 src/main/resources/static/css 目录中,因此 bootstrap.min.css 文件就放在这个目录,由于文件代码量多,并且是可以通过官网下载的,因此这里不贴出具体代码。

4、用户和角色

  • 用 JPA 创建用户和角色。

4.1、角色定义

  • 因为用户定义时,需要用到角色,因此先把角色类定义了,我习惯于先把被引用的类先创建和编辑。具体代码如下:
    package com.pyc.mysecurity.domain;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    
    @Entity
    public class SysRole {
        @Id
        @GeneratedValue
        private Long id;
        private String name;
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    
  • 项目运行后,JPA 自动在后台数据库创建对应的数据表。

4.2、用户定义

  • 创建另一个实体类,用于定义用户,代码如下:
    package com.pyc.mysecurity.domain;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import javax.persistence.*;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    // 令用户实体实现 UserDetails 接口,从而用户实体即为 Spring Security 所使用的用户
    // Make the user entity implement the UserDetails interface so that
    // the user entity is the user used by Spring Security
    @Entity
    public class SysUser implements UserDetails {
        private static final Long serialVersionUID=1L;
    
        @Id
        @GeneratedValue
        private Long id;
        private String username;
        private String password;
    
        // 配置用户和角色的多对多关系
        // Configure many-to-many relationships for users and roles
        @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
        private List<SysRole> roles;
    
        // 重写 getAuthorities 方法,将用户的角色作为权限
        // Overwrite the getAuthorities method so that can make the roles of user became authority
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities(){
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            List<SysRole> roles = this.getRoles();
            for(SysRole role:roles){
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            return authorities;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        public List<SysRole> getRoles() {
            return roles;
        }
    
        public void setRoles(List<SysRole> roles) {
            this.roles = roles;
        }
    }
    

5、数据表初始数据

  • 在 resources 目录下新建一个 data.sql,编辑如下:
    insert into SYS_USER(id, username, password)
    values (1, 'pyc', 'pyc');
    insert into SYS_USER(id, username, password)
    values (2, 'ycy', 'ycy');
    
    insert into SYS_ROLE(id, name) values (1, 'ROLE_ADMIN');
    insert into SYS_ROLE(id, name) values (2, 'ROLE_USER');
    
    insert into SYS_USER_ROLES(SYS_USER_ID, ROLES_ID) values (1, 1);
    insert into SYS_USER_ROLES(SYS_USER_ID, ROLES_ID) values (2,2);
    
  • 在第一次运行后,记得删除或改名。

6、传值对象

  • 测试不同角色的用户的数据展示,代码如下:
    package com.pyc.mysecurity.domain;
    
    public class Msg {
        private String title;
        private String content;
        private String etraInfo;
    
        public Msg(String title, String content, String etraInfo){
            super();
            this.content=content;
            this.title=title;
            this.etraInfo=etraInfo;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setContent(String content) {
            this.content = content;
        }
    
        public String getContent() {
            return content;
        }
    
        public void setEtraInfo(String etraInfo) {
            this.etraInfo = etraInfo;
        }
    
        public String getEtraInfo() {
            return etraInfo;
        }
    }
    

7、Repository

  • 因为用的是 JPA,因此需要实体类的 Repository,这里只需编辑一个按名称查找的方法,代码如下:
    package com.pyc.mysecurity.dao;
    
    import com.pyc.mysecurity.domain.SysUser;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface SysUserRepository extends JpaRepository<SysUser, Long> {
        SysUser findByUsername(String username);
    }
    

8、自定义 UserDetailsService

  • 为了符合要求,自定义一个 UserDetailsService,代码如下:
    package com.pyc.mysecurity.service;
    
    import com.pyc.mysecurity.dao.SysUserRepository;
    import com.pyc.mysecurity.domain.SysUser;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    
    // 自定义需实现 UserDetailsService 接口
    // Custom service needs to implement UserDetailsService interface
    @Service
    public class CustomUserService implements UserDetailsService {
    
        @Autowired
        SysUserRepository userRepository;
    
        // overwrite loadUserByUsername method to get account
        @Override
        public UserDetails loadUserByUsername(String username){
            SysUser user = userRepository.findByUsername(username);
            if(user == null){
                throw new UsernameNotFoundException("用户名不存在");
            }
            return user;
        }
    }
    

9、Config

  • Config 类有 Spring MVC 的和 Spring Security 的。

9.1、WebMvcConfig

  • 对 Spring MVC 进行配置
    package com.pyc.mysecurity.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    @Configuration
    public class WebMvcConfig extends WebMvcConfigurerAdapter {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/login").setViewName("login");
        }
    }
    

9.2、WebSecurityConfig

  • 对 Spring Security 进行配置,诸如登陆行为、用户授权和认证
    package com.pyc.mysecurity.config;
    
    import com.pyc.mysecurity.service.CustomUserService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    // 拓展的 Spring Security 配置需要继承 WebSecurityConfigurerAdapter
    // extend spring security need to extend WebSecurityConfigurerAdapter
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        // booking a bean of CustomUserService
        @Bean
        UserDetailsService customUserService(){
            return new CustomUserService();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 添加自定义的 user detail service 认证
            // add custom user detail service authentication
            auth.userDetailsService(customUserService());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // any request must to authorize so that can login
            http.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .failureUrl("/login?error")
                    .permitAll()
                    .and()
                    .logout().permitAll();
        }
    }
    

10、视图页面

  • 这里的视图页面主要有两个,一个登录页面和一个登录成功后展示的页面。

10.1、Login Page

  • 登陆页面用 Thymeleaf 编辑,如下:
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org" lang="en">
    <head>
        <meta content="text/html;charset=UTF-8"/>
        <title>登录</title>
        <link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
        <style type="text/css">
            body{
                padding-top: 50px;
            }
            .starter-template{
                padding: 40px 15px;
                text-align: center;
            }
        </style>
    </head>
    <body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <a class="navbar-brand" href="#">Spring Security Demo</a>
            </div>
            <div id="navbar" class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li><a th:href="@{/}">Home</a> </li>
                </ul>
            </div><!--/ .nav-collapse-->
        </div>
    </nav>
    <div class="container">
        <div class="starter-template">
    <!--        show after successfully logout-->
            <p th:if="${param.logout}" class="bg-warning">Successfully logout </p>
    <!--        show when happen some login error-->
            <p th:if="${param.error}" class="bg-danger">Happening some error, please try again</p>
            <h2>Login by account and password</h2>
    <!--        default login URL is "/login"-->
            <form name="form" th:action="@{/login}" action="/login" method="post">
                <div class="form-group">
                    <label for="username">Account:</label>
                    <input type="text" id="username" name="username" class="form-control" value="" placeholder="Account"/>
                </div>
                <div class="form-group">
                    <label for="password">Password:</label>
                    <input type="password" id="password" name="password" class="form-control" placeholder="Password"/>
                </div>
                <input type="submit" id="login" value="login" class="btn btn-primary"/>
            </form>
        </div>
    </div>
    </body>
    </html>
    
  • 页面内容比较简单,一个导航栏和一个登陆框。

10.2、Home Page

  • 该页面为登录成功后出现的页面,仍然用 Thymeleaf 编辑,如下:
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org" lang="en"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
    <!--Add support for Spring Security-->
    <head>
        <meta content="text/html;charset=UTF-8"/>
        <title sec:authentication="name"></title>
        <link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
        <style type="text/css">
            body{
                padding-top: 50px;
            }
            .starter-template{
                padding: 40px 15px;
                text-align: center;
            }
        </style>
    </head>
    <body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <a class="navbar-brand" href="#">Spring Security Demo</a>
            </div>
            <div id="navbar" class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li><a th:href="@{/}">Home</a> </li>
                </ul>
            </div><!--/ .nav-collapse-->
        </div>
    </nav>
    <div class="container">
        <div class="starter-template">
            <h1 th:text="${msg.title}"></h1>
            <p class="bg-primary" th:text="${msg.content}"></p>
            <div sec:authorize="hasRole('ROLE_ADMIN')">
                <p class="bg-info" th:text="${msg.etraInfo}"></p>
            </div>
            <div sec:authorize="hasRole('ROLE_USER')">
                <p class="bg-info">Not too much message to display</p>
            </div>
            <form th:action="@{/logout}" method="post">
                <input type="submit" class="btn btn-primary" value="logout"/>
            </form>
        </div>
    </div>
    </body>
    </html>
    
  • 页面由一个导航栏和简单的页面内容构成,页面内容根据用户角色的不同而展示不同信息。

11、controller

  • 最后编辑一个 Controller,代码如下:
    package com.pyc.mysecurity.web;
    
    import com.pyc.mysecurity.domain.Msg;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class WebController {
        @RequestMapping("/")
        public String index(Model model){
            Msg msg = new Msg("Demo Title",
                    "Demo Content",
                    "additional msg, only admin can see");
            model.addAttribute("msg",msg);
            return "home";
        }
    }
    
  • 入口类无需修改和编辑。

四、项目测试

  • 运行项目测试功能是否正常。初始页面如下:
    在这里插入图片描述

1、成功登录

  • 先测试成功登录的情况。

1.1、管理员登录

  • 测试以管理员身份登录,主页页面显示内容是否正常。登录成功后页面如下:
    在这里插入图片描述
  • 标签页标签显示信息:
    在这里插入图片描述

1.2、用户登录

  • 用用户角色的用户登录,页面如下:
    在这里插入图片描述
  • 标签页标签显示信息:
    在这里插入图片描述
  • 主页显示情况正常,管理员和普通用户显示不同的信息。

2、注销测试

  • 无论是管理员用户登录,还是普通用户登录,注销后页面如下:
    在这里插入图片描述
  • 显示正常。

3、错误登录测试

  • 用不正确的用户密码登录
    在这里插入图片描述
  • 显示正常。

4、程序控制台

  • 在前台进行登录、注销等行为时,控制台输出关键 SQL 语句信息:
    在这里插入图片描述

5、查看后台数据库

  • 用管理工具打开后台数据库,可以发现数据库多了三个数据表:
    在这里插入图片描述
  • 三个表的内容:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 可以总结,项目运行正常。

上一篇
下一篇

发布了184 篇原创文章 · 获赞 24 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42896653/article/details/104293802