Spring Security:在架构上将认证与授权分离,并提供了扩展点
Spring IoC(控制反转),DI(依赖注入)和 AOP(面向切面编程)功能Spring 的应用程序提供身份验证和授权支持Spring MVC 有很好地集成 ,并配备了流行的安全算法实现捆绑在一起Filter;对方法调用进行保护,最好于 AOP
Spring Security 在进行用户认证以及授予权限时通过各种各样的拦截器来控制权限的访问,从而实现安全Spring Security 对 Web 安全性的支持大量地依赖于 Servlet 过滤器
Spring Security 提供有若干个过滤器,能够拦截 Servlet 请求
Servlet 过滤器且令其正常工作必须在 web.xml 文件中使用 和 元素配置
FilterToBeanProxy 是特殊的Servlet过滤器
Spring 应用程序上下文中的一个 Bean 来完成
Bean 几乎和 Servlet 过滤器一样
javax.servlet.Filter 接口Spring 配置文件非 web.xml 文件中配置FilterToBeanProxy 代理给的 Bean 可以是 javax.servlet.Filter 的任意实现
Spring Security 的任何过滤器
Spring Security 要求至少配置四个而且可能一打或者更多的过滤器shiro 的功能都有
Spring Security 对 Oauth、OpenID 也有支持
注:
添加相应依赖信息:包括 Thymeleaf 整合 security 依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter:自定义 Security 策略AuthenticationManagerBuilder:自定义认证策略@EnableWebSecurity:开启 WebSecurity 模式// AOP 实现
@EnableWebSecurity
public class SecurityConf extends WebSecurityConfigurerAdapter {
// 请求授权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 链式编写:不同模块使用 and() 连接,也可以分开写
// 对请求授权
http.authorizeRequests()
// 不需要通过登录验证就可以被访问的资源路径
.antMatchers("/", "/index", "/toLogin").permitAll()
// 需要对应权限、角色访问
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3")
// 上面的任何请求都必须经过身份验证
.anyRequest().authenticated();
// 表单登录,用户未登录时,访问任何资源都转跳到该路径,即登录页面
http.formLogin()
// 登录页面,设置为自定义的登录页面
.loginPage("/toLogin")
// 登录表单中 action 的地址,即实际处理认证请求的路径
.loginProcessingUrl("/login")
// 获取用户名输入框参数:默认 username
.usernameParameter("username")
// 获取密码输入框参数:默认 password
.passwordParameter("password")
// 登录认证成功后默认转跳的路径,登陆失败后返回原页面
.successForwardUrl("/").failureForwardUrl("/toLogin")
.permitAll()
// 注销
http.logout()
// 登出成功后返回的请求
.logoutSuccessUrl("/")
.permitAll()
// 关闭 csrf 功能:跨站请求伪造,默认只能通过 post 方式提交请求
http.csrf().disable()
// 记住我功能,使用 Cookie 缓存,默认有效期14天
.rememberMe().rememberMeParameter("remember");
}
}
?:匹配任何单字符*:匹配 0 或者任意数量的字符**:匹配 0 或更多目录permitAll():允许任何访问denyAll():拒绝所有访问anonymous():允许匿名用户访问authenticated():允许认证的用户进行访问hasRole(String) :
hasAnyRole(String…) :
hasAuthority(String):
hasAnyAuthority(String…) :
principal:
rememberMe() :
Remember-me功能认证允许访问fullyAuthenticated():
hasIpAddress(String):
isAnonymous():当前委托人是否为匿名用户isRememberMe():当前主体是否是“记住我”的用户not() :对其他访问结果求反AuthenticationManagerBuilder:使用构造者方式来构建
inMemoryAuthentication()
passwordEncoder()
Spring security5后,必须指定加密方式,不然程序会报错withUser()、password()、authorities()
and() 方法来连接下一个用户的添加@EnableWebSecurity
public class SecurityConf extends WebSecurityConfigurerAdapter {
// 权限认证
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存里面存储用户的身份认证和授权信息
auth.inMemoryAuthentication()
// 添加用户权限:用户名、密码(加密)、角色信息
.withUser("user").password(passwordEncoder().encode("123456")).roles("vip1")
// 添加多个用户使用 and()
.and()
.withUser("admin").password(passwordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
// 配置BCrypt加密
.and().passwordEncoder(passwordEncoder());
}
// 加密对象,密码需要加密才能安全保存
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
jdbcAuthentication()
jdbc 的方式来查询用户和权限dataSource():指定数据库连接信息passwordEncoder():指定密码加密规则
usersByUsernameQuery()、authoritiesByUsernameQuery()
Spring security 默认了查询用户、权限甚至还有群组用户授权的sql
org......userdetails.jdbc.JdbcDaoImpl 中// 自动装配数据源对象
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用 JDBC 存储身份认证信息
auth.jdbcAuthentication()
// 传入数据源
.dataSource(dataSource)
// 指定密码加密方式
.passwordEncoder(passwordEncoder())
// 根据用户名获取用户信息
.usersByUsernameQuery("select username, password, status from Users where username = ?")
// 根据用户名获取认证权限
.authoritiesByUsernameQuery("select username, authority from Authority where username = ?");
}
//定义加密对象
@Bean
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
LDAP:轻型目录访问协议,一个开放的,中立的,工业标准的应用协议
userSearchFilter()、groupSearchFilter()
userSearchBase()、groupSearchBase()
contextSource().url():
LDAP服务器的地址contextSource().root() 使用嵌入式 LDAP 服务器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> configurer = auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}");
configurer.passwordCompare()
.passwordEncoder(passwordEncoder())
.passwordAttribute("passcode");
configurer.contextSource().url("ldap://xxxxx.com:33389/dc=xxxxxx,dc=com");
}
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
自定义认证方式:不仅限于 MyBatis 使用
UserDetailsService 的 service 类
loadUserByUsername()
username 来匹配带有密码等信息的用户实体
User 类需要实现 UserDetailsSpring Security 所需要的信息Spring Security 的 User类中
User 和 Spring Security 的 UserSpring Security 拿前台的数据比较,做出操作
@Service
public class UserService implements UserDetailsService {
@Qualifier("userDao")
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username == null){
throw new UsernameNotFoundException("用户名不存在");
}
// 获取用户对象
security.pojo.User user = userDao.getUserByName(username);
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
// 得到用户权限
String role = user.getRoles();
// 权限不为空放到到 List 集合中
if (role != null && !"".equals(role)){
authorityList.add(new SimpleGrantedAuthority(role.trim()));
}
// 封装到框架的 USer 类中并提交
return new User(user.getUsername(),user.getPassword(),authorityList);
}
}
Spring Security 自定义的 UserDetailsService 实现类
loadUserByUsername() 来查找用户@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
//密码加密方式和数据库密码加密方式要相同
.passwordEncoder(passwordEncoder());
}
@Bean
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
<form th:action="@{/login}" method="post">
<input type="text" placeholder="Username" name="username">
<input type="password" placeholder="PassWord" name="password">
<input type="checkbox" class="ui checkbox bordered" name="remember">保存账号密码
<input type="submit" value="提交"/>
form>
整合 Thymeleaf 模板使用
命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div sec:authorize="!isAuthenticated()"/>
<div sec:authorize="isAuthenticated()"/>
<div sec:authorize="hasAnyRole('vip1','vip2','vip3')"/>
<div sec:authorize="hasRole('vip1')"/>
<span sec:authentication="principal.authorities">
Apache Shiro:一个强大且易用的 Java 安全框架
Subject、 SecurityManager 和 RealmsSubject:当前操作用户
Shiro 中,Subject 并不仅仅指人
Subject 代表当前用户的安全操作,SecurityManager 则管理所有用户的安全操作SecurityManager:Shiro 框架的核心,典型的 Facade 模式
SecurityManager 来管理内部组件实例,并提供安全管理的各种服务Realm:充当 Shiro 与应用安全数据间的 桥梁 或 连接器
Shiro 从应用配置的 Realm 中查找用户及其权限信息
Realm 实质上是安全相关的 DAO
ShiroShiro 时,必须至少指定一个 Realm,用于认证和(或)授权
Realm 是可以的,但至少需要一个Shiro 内置了可连接大量安全数据源(又名目录)的 Realm
LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等Realm 不能满足需求,可以插入代表自定义数据源的 Realm 实现组件
UsernamePasswordToken:
Shiro 用来封装用户登录信息,使用用户的登录信息创建令牌 TokenShiro 验证令牌是否具有合法身份以及相关权限AuthenticationInfo:用户的角色信息集合,认证时使用AuthorizationInfo,角色的权限信息集合,授权时使用DefaultWebSecurityManager:安全管理器
Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效ShiroFilterFactoryBean:过滤器工厂
Shiro 的基本运行机制是开发者定制规则,Shiro 去执行ShiroFilterFactoryBean 创建 Filter 对象来完成功能模块
Session Manager:会话管理,用户登录后的session相关管理Cryptography:加密,密码加密等Web Support:Web支持,集成Web环境Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去Testing:测试支持Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问Remember Me:记住我
Java Security API
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.1.0version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-web-starterartifactId>
<version>1.8.0version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-to-slf4jartifactId>
dependency>
//仅有部分相关方法
// 要登录请求处理:跳转到登录页面
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
// 登录处理
@RequestMapping("/login")
public String login(String username, String password, Model model) {
// 获取当前用户 subject 对象
Subject subject = SecurityUtils.getSubject();
// 使用传进来的用户名、密码创建令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
// 执行登录操作:由框架进行处理
// 认证、授权在 realm 中
// 拦截规则在过滤器中定义
subject.login(token);
return "index";
} catch (UnknownAccountException e) {
// 拦截异常情况
model.addAttribute("message", "用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("message", "密码错误");
return "login";
}
}
// 认证异常请求处理
@RequestMapping("/unauthorized")
@ResponseBody
public String unauthorized(){
return "未获得权限,无法访问";
}
public class UserRealm extends AuthorizingRealm {
// 注入对象从数据库中查找用户信息
@Autowired
private UserDao userDao;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 得到认证时传入的用户对象(或用户名、id 等对象)
User user = (User)principalCollection.getPrimaryPrincipal();
// 或得到 Subject 当前用户对象
// Subject subject = SecurityUtils.getSubject();
// 转为自定义的 User 对象
// User user = (User)subject.getPrincipal();
// 硬授权:所有进入的用户都会授权 user:add
// info.addStringPermission("user:add");
// 获取用户角色信息并添加角色(或使用授权)
info.addRole(user.getRoles());
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 得到传进来的用户名
String username = (String) authenticationToken.getPrincipal();
// 通过用户名查询用户
User user = userDao.getByName(username);
// 如果用户为 null
if (user == null) {
// 不正确时直接返回 null,会自动捕捉异常
return null;
}
// 传入用户对象、密码、自定义 Realm 类名传进去由框架自动匹配
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
/*
第一个参数可以是用户对象、用户名、用户id 等
可以在授权时获取此时传入的信息,密码会由框架自动匹配
*/
}
}
CustomRealm 和 SecurityManager 等注入到 spring 容器MyRealm
DefaultWebSecurityManager
MyRealm 注入到 DefaultWebSecurityManager 完成注册ShiroFilterFactoryBean
Filter 工厂实例@Configuration
public class ShiroCong {
// Realm 对象,将自定义验证方式加入容器
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
// DefaultWebSecurityManager: 安全管理器
// 权限管理,配置主要是 Realm 的管理认证
@Bean
public DefaultWebSecurityManager securityManager(){
// 关联 Realm 对象
return new DefaultWebSecurityManager(userRealm());
}
// ShiroFilterFactoryBean:过滤器对象
// Filter 工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 关联 SecurityManager 对象
bean.setSecurityManager(securityManager());
// 使用 LinkHashMap 存放路径拦截规则,有序:先配置先生效
Map<String, String> filterMap = new LinkedHashMap<>();
// 默认无法配置多角色访问同一权限,需要自定义修改 shiro 过滤器配置
// 授权: 有 add 角色才能访问 add 请求
filterMap.put("/user/add", "roles[add]");
// 授权:必须有 user:update 权限才能访问 update
filterMap.put("/user/update", "perms[user:update]");
// 拦截: 对 user下资源必须登陆后才能访问
filterMap.put("/user/**", "authc");
// 登出
filterMap.put("/logout", "logout");
// 对所有用户认证必须登录后访问
filterMap.put("/**", "authc");
// 将设置了请求权限的 Map 集合放到 过滤器对象中
bean.setFilterChainDefinitionMap(filterMap);
// 未登录跳转到登录页
bean.setLoginUrl("/toLogin");
// 登陆成功请求;跳转首页
bean.setSuccessUrl("/index");
// 认证异常请求处理:权限不足
bean.setUnauthorizedUrl("/unauthorized");
return bean;
}
// 配置 ShiroDialect 方言标签整合 Thymeleaf
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
自定义 Shiro 过滤器
AuthorizingRealm,实现两个抽象方法分别完成授权和认证的逻辑anon:无需认证即可访问,游客身份authc:必须认证、登录 才能访问authcBasic:需要通过 httpBasic 认证user:不一定已通过认证
rememberMeperms:必须拥有对某个资源的访问权限(授权)才能访问role:必须拥有某个角色权限才能访问port:请求的端口必须为指定值才可以访问rest:请求必须是 RESTful
ssl:必须是安全的 URL 请求,协议为 HTTPSAuthenticationException:认证异常
Shiro 在登录认证过程中,认证失败需要抛出的异常
CredentitalsException:凭证异常
IncorrectCredentialsException:不正确的凭证ExpiredCredentialsException:凭证过期AccountException:账号异常
ConcurrentAccessException:并发访问异常,多个用户同时登录时抛出UnknownAccountException:未知的账号ExcessiveAttemptsException:认证次数超过限制DisabledAccountException: 禁用的账号LockedAccountException:账号被锁定UnsupportedTokenException:使用了不支持的TokenAuthorizationException: 授权异常
Shiro在登录认证过程中,授权失败需要抛出的异常
UnauthorizedException
UnanthenticatedException
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="Thymeleaf"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8" />
<title>Insert title heretitle>
head>
<body>
<h3>indexh3>
<p shiro:guest="">Please <a href="login.html">logina>p>
<p shiro:user="">
Welcome back John! Not John? Click <a href="login.html">herea> to login.
p>
<p shiro:authenticated="">
Hello, <span shiro:principal="">span>, how are you today?
p>
<a shiro:authenticated="" href="updateAccount.html">Update your contact informationa>
<p>Hello, <shiro:principal/>, how are you today?p>
<p shiro:notAuthenticated="">
Please <a href="login.html">logina> in order to update your credit card information.
p>
<a shiro:hasRole="admin" href="admin.html">Administer the systema>
<p shiro:lacksRole="developer">
Sorry, you are not allowed to developer the system.
p>
<p shiro:hasAllRoles="developer, 2">
You are a developer and a admin.
p>
<p shiro:hasAnyRoles="admin, vip, developer,1">
You are a admin, vip, or developer.
p>
<a shiro:hasPermission="userInfo:add" href="createUser.html">添加用户a>
<p shiro:lacksPermission="userInfo:del">
Sorry, you are not allowed to delete user accounts.
p>
<p shiro:hasAllPermissions="userInfo:view, userInfo:add">
You can see or add users.
p>
<p shiro:hasAnyPermissions="userInfo:view, userInfo:del">
You can see or delete users.
p>
<a shiro:hasPermission="pp" href="createUser.html">Create a new Usera>
body>
html>