认证与授权
- 为了系统具有一定的安全性,也为了将普通用户和管理员用户区分开来,大多数企业级的 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、查看后台数据库
- 用管理工具打开后台数据库,可以发现数据库多了三个数据表:
- 三个表的内容:
- 可以总结,项目运行正常。