- 1、简介:
- 2、Spring Security
- 3、注解开发
- 4、记住我
- 5、Thymeleaf中SpringSecurity的使用
- 6、Oauth2认证
- 7、JWT
1、简介:
1.1、安全框架概述
- 什么是安全框架?
- 解决系统安全问题的框架。
- 如果没有安全框架
- 需要手动处理(手动编写servlet每个资源的访问控制,非常麻烦)。
- 使用安全框架
- 可以通过配置的方式实现对资源的访问限制。
1.2、常用安全框架
Spring Security:
- 基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
- 提供了一组可以在Spring应用上下文中配置的Bean,充分利用了 Spring IOC , DI(控制反转Inversion of Control,DI:Dependency Injection 依赖注入)和AOP(面向切面编程) 功能。
- 对于authentication和authorization,Spring Security 框架都有很好的支持。
- 在用户认证(authentication)方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
- 在用户授权方面(authorization),Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
Apache Shiro:
- 一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。
- 可定制化
- 认证(authentication)和授权(authorization)
1.3、概述
**Spring Security是一个高度自定义的安全框架。利用 Spring IOC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。**
**使用 Spring Secruity 的原因有 很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到它们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。 正如你可能知道的两个应用程序的两个主要区域是==“认证”****和****“授权”==(或者访问控制)。**
**这两点也是 Spring Security 重要核心功能。**
- “认证”,是建立一个声明主体的过程(一个“主体”一般是指用户, 设备或一些可以在应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。
- “授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否 有权限去做某些事情。
2、Spring Security
2.1、快速入门
pom.xml
<!--SpringSecurity组件-->
<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>
<!--test组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
首页
controller
@RequestMapping("/login",method = RequestMethod.POST)
public String login() {
return "redirect:main.html";
}
注意:
- 不自定义配置下,它跳转的是spring security的默认登录界面
- 用户名 默认user,密码自动生成
2.2、UserDetailsService
public interface UserDetailsService {
//通过用户名先认证 数据库没有此用户会抛出异常 有则跳转到UserDetails进一步认证
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();//密码
String getUsername();//用户名
boolean isAccountNonExpired();//帐户未过期
boolean isAccountNonLocked();//帐户未锁定
boolean isCredentialsNonExpired();//凭证未过期
boolean isEnabled();//能够使用
}
//UserDetails是接口,要找具体实现类User
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private static final Log logger = LogFactory.getLog(User.class);
private String password;//密码
private final String username;//用户名
private final Set<GrantedAuthority> authorities;//授权
private final boolean accountNonExpired;//帐户未过期
private final boolean accountNonLocked;//帐户未锁定
private final boolean credentialsNonExpired;//凭证未过期
private final boolean enabled;//能够使用
/*构造器
三个参数 username password authorities 用户具有的权限。此处不允许为 null
*/
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
/*构造器重载
七个参数:username password
enabled accountNonExpired
credentialsNonExpired accountNonLocked
authorities 用户具有的权限。此处不允许为 null
*/
public User(String username, String password,
boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
}
/*此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。
SpringSecurity 会根据 User 中的 password 和客户端传递过来的 password 进行比较。
如果相同则表示认证通过,如果不相同表示认证失败
*/
- authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限, 如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。
- 通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象 的。参数是一个字符串,多个权限使用逗号分隔。
- 方法参数 方法参数表示用户名。
- 此值是客户端表单传递过来的数据。默认情况下必须叫 username ,否则无法接收
2.3、PasswordEncoder 密码解析器详解
源码:
public interface PasswordEncoder {
/**
* 加密.
* SHA-1
* or greater hash combined with an 8-byte 哈希
or greater randomly generated salt. 盐值处理
*/
String encode(CharSequence rawPassword);
/*验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。
如果密码匹配,则返回 true;
如果不匹配,则返回 false。
第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
//可二次加密 返回true
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
//多个实现类 推荐使用BCryptPasswordEncoder
public class BCryptPasswordEncoder implements PasswordEncoder {
private Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
private final Log logger = LogFactory.getLog(getClass());
private final int strength;//默认10
private final BCryptVersion version;
private final SecureRandom random;
//枚举类型版本号
public enum BCryptVersion {
$2A("$2a"),
$2Y("$2y"),
$2B("$2b");
private final String version;
BCryptVersion(String version) {
this.version = version;
}
public String getVersion() {
return this.version;
}
}
}
构造器:
/**
* 三个参数:
* BCryptVersion version,//版本号
* int strength, //长度
* SecureRandom random//随机数
* 方法重载
*/
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
this(strength, null);
}
public BCryptPasswordEncoder(BCryptVersion version) {
this(version, null);
}
public BCryptPasswordEncoder(BCryptVersion version, SecureRandom random) {
this(version, -1, random);
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
this(BCryptVersion.$2A, strength, random);
}
public BCryptPasswordEncoder(BCryptVersion version, int strength) {
this(version, strength, null);
}
public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
throw new IllegalArgumentException("Bad strength");
}
this.version = version;
this.strength = (strength == -1) ? 10 : strength;
this.random = random;
}
}
加密:
//加密 盐值 随机数 保证相同密码每次加密结果不一样
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt = getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
//获取盐值
private String getSalt() {
if (this.random != null) {
return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
}
return BCrypt.gensalt(this.version.getVersion(), this.strength);
}
密码匹配:
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
二次加密:
//二次加密
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
}
int strength = Integer.parseInt(matcher.group(2));
return strength < this.strength;
}
测试:
@Test
public void contextLoads() {
PasswordEncoder encoder=new BCryptPasswordEncoder();
String encode = encoder.encode("123");
System.out.println(encode);
boolean matches = encoder.matches("123", encode);
System.out.println("===========================");
System.out.println(matches);
}
2.4、自定义登录逻辑
- 当进行自定义登录逻辑时需用到UserDetailsService 和 PasswordEncoder
- 但是 Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder实例。
- 不能直接 new 对象。
2.4.1、登录常用参数
登录成功或失败
formLogin() 基于表单登录
loginPage() 登录页
defaultSuccessUrl 登录成功后的默认处理页
failuerHandler 登录失败之后的处理器
successHandler 登录成功之后的处理器
failuerUrl 登录失败之后系统转向的url,默认是this.loginPage + "?error"
注销
logoutUrl 登出url,默认是/logout, 它可以是一个ant path url
logoutSuccessUrl 登出成功后跳转的 url 默认是"/login?logout"
logoutSuccessHandler 登出成功处理器,设置后会把logoutSuccessUrl 置为null
2.4.2、编写配置类:
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
2.4.3、自定义逻辑
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("执行了loadUserByUsername方法");
//1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException异常
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
//2.把查询出来的密码(注册时已经加密过)进行解析,或者直接把密码放入构造方法
String password = pw.encode("root");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
2.4.4、自定义登录界面
Spring Security 提供了登录页面,但是对于实际项目中,会自定义登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapter,并重写configure 方法。
- successForwardUrl() :登录成功后跳转地址 loginPage() :
- loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
- antMatchers() :匹配内容
- permitAll() :允许所有
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义登录页面
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain");
//授权认证
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//所有请求都必须被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭csrf防护关闭 跨站请求伪造
http.csrf().disable();
}
修改controller
@RequestMapping(value = "/toMain",method = RequestMethod.POST)
public String toMain() {
return "redirect:main.html";
}
登陆失败
controller
@RequestMapping("/toError",method = RequestMethod.POST)
public String toError() {
return "redirect:error.html";
}
修改配置类
http.formLogin().failureForwardUrl("/toError");
//放行toError
http.authorizeRequests().antMatchers("/error.html").permitAll();
error.hmtl
跳转
2.4.5、自定义入参
源码
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
- public static final String SPRING_SECURITY_FORM_USERNAME_KEY = “username”;
- public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = “password”;
http.formLogin()
//自定义入参
.usernameParameter("uname")
.passwordParameter("pword")
2.4.6、设置登录处理器
登录成功
源码分析
AuthenticationSuccessHandler
成功处理器接口 ——> 实现类ForwardAuthenticationSuccessHandler
public interface AuthenticationSuccessHandler {
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
onAuthenticationSuccess(request, response, authentication);
//放行
chain.doFilter(request, response);
}
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String forwardUrl;
public ForwardAuthenticationSuccessHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> "'" + forwardUrl + "' is not a valid forward URL");
this.forwardUrl = forwardUrl;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//转发 不能跳转外部资源,不适用前后端分离项目
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
- ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。
- 由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取拥有权限的人
Object getCredentials();//获取密码
Object getDetails();
Object getPrincipal();//获取用户信息
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
自定义处理器
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException,ServletException {
//Principal 主体,存放了登录用户的信息
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
//保密 输出null
System.out.println(user.getPassword());
System.out.println(user.getAuthorities());
//重定向url
response.sendRedirect(url);
}
}
修改配置类
http.formLogin()
//登录成功后的处理器,不能和successForwardUrl共存
.successHandler(new MyAuthenticationSuccessHandler("/login.html")
登录失败
源码:
AuthenticationFailureHandler
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
hrows IOException, ServletException;
}
实现类:ForwardAuthenticationFailureHandler
public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String forwardUrl;
public ForwardAuthenticationFailureHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> "'" + forwardUrl + "' is not a valid forward URL");
this.forwardUrl = forwardUrl;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
//转发
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
自定义处理器
public class MyFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
修改配置类
http.formLogin()
//登录失败后的处理器,不能和failureForwardUrl共存
.failureHandler(new MyAuthenticationFailurHandler("/error.html"))
2.4.7、访问控制url匹配
- 在前面认证中所有常用配置,主要是对 http.formLogin() 进行操作。
- 而在配置类中 http.authorizeRequests() 主要是对url进行控制,也就是授权(访问控制)。
- http.authorizeRequests() 也支持连缀写法,总体公式为:
- url 匹配规则.权限控制方法 通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。
- 这些内容进行各种组合就形成了 Spring Security中的授权。
requestMatchers() 配置一个request Mather数组,参数为RequestMatcher 对象,其match 规则自定义,需要的时候放在最前面,对需要匹配的的规则进行自定义与过滤
authorizeRequests() URL权限配置
antMatchers() 配置一个request Mather 的 string数组,参数为 ant 路径格式, 直接匹配url
anyRequest() 匹配任意url、无参,最好放在最后面
anyRequest()
- 任何请求,必须放在最后,有严格的执行顺序
//所有请求必须认证之后才能访问
http.authorizeRequests()
.anyRequest().authenticated();
antMatcher()
常用作静态资源放行
antMatchers(String... antPatterns)
antMatchers(HttpMethod method, String... antPatterns)
- 参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
- 规则如下:
- ? : 匹配一个字符
- *:匹配 0 个或多个字符
- ** :匹配 0 个或多个目录
在实际项目中经常需要放行所有静态资源
http.authorizeRequests()
//放行相关目录下静态资源
.antMatchers("/js/**","/image/**","/css/**").permitAll()
//放行所有静态资源
.antMatchers("/**").permitAll()
regexMatchers()
- 正则表达式
regexMatchers(String... regexPatterns)
//默认匹配所有方法,也可手动定义
regexMatchers(HttpMethod method, String... regexPatterns)
- Controller
@RequestMapping(value = "/demo",method = RequestMethod.GET)
@ResponseBody
public String demo() {
return "demo";
}
- 新增配置
http.authorizeRequests()
.regexMatchers(".+[.]jpg").permitAll()
.regexMatchers(".+[.]css").permitAll()
//匹配方法
.regexMatchers(HttpMethod.GET,"/demo").permitAll()
mvcMatchers()
- servletPath 就是所有的 URL 的统一前缀。
- 在 SpringBoot 整合SpringMVC 的项目中可以在 application.properties 中添加下面内容设置 ServletPath
- servletPath(项目目录) mvcMatchers独有
常用形式
mvcMatchers(HttpMethod method, String... mvcPatterns)
mvcMatchers(String... patterns)
新增配置
http.authorizeRequests()
.mvcMatchers("/demo").servletPath("/xxx").permitAll()
//也可写成,等效
.antMatchers("/xxx/demo").permitAll()
application.properties 中新增
spring.mvc.servlet.path=/xxx
2.4.8、内置访问控制方法
Spring Security 匹配了 URL 后调用了 permitAll() 表示不需要认证,随意访问。
在 Spring Security 中提供了6种内置控制。
static final String permitAll = "permitAll"; //允许所有
private static final String denyAll = "denyAll"; //禁止访问
private static final String anonymous = "anonymous"; //匿名访问
private static final String authenticated = "authenticated"; //认证访问
private static final String fullyAuthenticated = "fullyAuthenticated"; //输账号密码才能进入
private static final String rememberMe = "rememberMe"; //记住我
permitAll()
- permitAll()表示所匹配的 URL 任何人都允许访问。
public ExpressionInterceptUrlRegistry permitAll() {
return access(permitAll);
}
authenticated()
- authenticated()表示所匹配的 URL 都需要被认证才能访问。
public ExpressionInterceptUrlRegistry authenticated() {
return access(authenticated);
}
anonymous()
- anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中
public ExpressionInterceptUrlRegistry rememberMe() {
return access(rememberMe);
}
denyAll()
- denyAll()表示所匹配的 URL 都不允许被访问
public ExpressionInterceptUrlRegistry denyAll() {
return access(denyAll);
}
rememberMe()
- 被“remember me”的用户允许访问
fullyAuthenticated()
- 如果用户不是被 remember me 的,才可以访问。
- 必须手动输入账号密码
public ExpressionInterceptUrlRegistry fullyAuthenticated() {
return access(fullyAuthenticated);
}
2.4.9、角色权限判断
除了之前的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。
基于权限控制
源码
//添加单个角色
public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
return this.access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}
//添加多个角色
public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) {
return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities));
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>main1</title>
</head>
<body>
<h3>main1</h3>
</body>
</html>
新增配置
http.authorizeRequests()
//一个权限
//.antMatchers("/main1.html").hasAuthority("admin")
//多个权限
.antMatchers("/main1.html").hasAnyAuthority("admin","admad")
基于角色控制
- 如果用户具备给定角色就允许访问。否则出现 403。
- 参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。
- 在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。
- 例如:ROLEabc 其中 abc 是角色名,ROLE是固定的字符开头。 使用 hasRole()时参数也只写 abc 即可。否则启动报错。
源码
public ExpressionInterceptUrlRegistry hasRole(String role) {
return access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
}
public ExpressionInterceptUrlRegistry hasAnyRole(String... roles) {
return access(ExpressionUrlAuthorizationConfigurer.hasAnyRole(roles));
}
新增配置类
http.authorizeRequests()
//基于角色
.antMatchers("/main1.html").hasRole("abc")
.antMatchers("/main1.html").hasAnyRole("abc","asd")
service
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));
基于IP地址控制
- hasIpAddress(String) 如果请求是指定的 IP 就运行访问。
- 可以通过 request.getRemoteAddr() 获取 ip 地址。
- 需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
源码:
public ExpressionInterceptUrlRegistry hasIpAddress(String ipaddressExpression) {
return access(ExpressionUrlAuthorizationConfigurer.hasIpAddress(ipaddressExpression));
}
新增配置类
http.authorizeRequests()
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
2.4.10、自定义403处理方案
新建类实现 AccessDeniedHandler
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
//设置响应状态码
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
//设置响应头
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
String message = "{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}";
writer.write(message);
writer.flush();
writer.close();
}
}
修改配置类
//异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
2.4.11、基于表达式的访问控制
1、access()方法使用
**之前登录用户权限判断实际上底层实现都是调用access(表达式)**,**可以通过 access() 实现和之前学习的权限控制完成相同的功能。**
官网:https://docs.spring.io/spring-security/site/docs/5.2.0.RELEASE/reference/html5/
取值:
hasRole(String role) |
Returns true if the current principal has the specified role.For example, hasRole('admin') By default if the supplied role does not start with ‘ROLE_’ it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler . |
---|---|
hasAnyRole(String… roles) |
Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings).For example, hasAnyRole('admin', 'user') By default if the supplied role does not start with ‘ROLE_’ it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler . |
hasAuthority(String authority) |
Returns true if the current principal has the specified authority.For example, hasAuthority('read') |
hasAnyAuthority(String… authorities) |
Returns true if the current principal has any of the supplied authorities (given as a comma-separated list of strings)For example, hasAnyAuthority('read', 'write') |
principal |
Allows direct access to the principal object representing the current user |
authentication |
Allows direct access to the current Authentication object obtained from the SecurityContext |
permitAll |
Always evaluates to true |
denyAll |
Always evaluates to false |
isAnonymous() |
Returns true if the current principal is an anonymous user |
isRememberMe() |
Returns true if the current principal is a remember-me user |
isAuthenticated() |
Returns true if the user is not anonymous |
isFullyAuthenticated() |
Returns true if the user is not an anonymous or a remember-me user |
hasPermission(Object target, Object permission) |
Returns true if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read') |
hasPermission(Object targetId, String targetType, Object permission) |
Returns true if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read') |
源码:
public ExpressionInterceptUrlRegistry access(String attribute) {
if (this.not) {
attribute = "!" + attribute;
}
interceptUrl(this.requestMatchers, SecurityConfig.createList(attribute));
return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}
修改配置类:
http.authorizeRequests()
.antMatchers("/login.html").access("permitAll()")
.antMatchers("/login.html").access("permitAll")
.antMatchers("/main1.html").access("hasRole('abc')")
2、自定义assert
- 虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自定义逻辑的情况。
- 判断登录用户是否具有访问当前 URL 权限。
service及其实现类
public interface MyService {
boolean hasPermission(HttpServletRequest request, Authentication
authentication);
}
@Service
public class MyServiceImpl implements MyService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//获取主体
Object o1 = authentication.getPrincipal();
if (o1 instanceof UserDetails){
//强转
UserDetails userDetails = (UserDetails) o1;
//获取权限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//判断当前uri是否有权限
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
修改配置类
http.authorizeRequests()
//.anyRequest().authenticated();
.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
access 中通过@bean的id名.方法(参数)的形式进行操作
3、注解开发
- 在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过 @EnableGlobalMethodSecurity 进行开启后使用。
- 如果设置的条件允许,程序正常执行。如果不允许会报 500
- 这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。
- 通常情况下 都是写在控制器方法上的,控制接口URL是否允许被访问。
**prePostEnabled**
: 确定 前置注解[@PreAuthorize,@PostAuthorize,..]
是否启用
**securedEnabled**
: 确定安全注解 [@Secured]
是否启用
**jsr250Enabled**
: 确定 JSR-250注解 [@RolesAllowed..]
是否启用
3.1、@Secured
@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。
主启动类
@EnableGlobalMethodSecurity(securedEnabled = true)
配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//url拦截
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//所有请求都必须被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}
controller加@Secured
@Secured(value = "ROLE_acb")
@RequestMapping(value = "/toMain",method = RequestMethod.POST)
public String toMain() {
return "redirect:main.html";
}
3.2、@PreAuthorize/@PostAuthorize
源码:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
/**
* @return the Spring-EL expression to be evaluated before invoking the protected
* method
*/
String value();
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PostAuthorize {
/**
* @return the Spring-EL expression to be evaluated after invoking the protected
* method
*/
String value();
}
- @PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
@PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。
@PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问
- @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
- @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
- @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
主启动类开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
controller新增注解
//@PreAuthorize("hasRole('abc')")
//PreAuthorize的表达式可以以ROLE_开头,也可以不以ROLE_开头,配置类必须ROLE_开头
@PreAuthorize("hasRole('ROLE_abc')")
- @PreAuthorize里面的参数是access表达式
- 其中hasRole的值可以为角色名,也可以加ROLE_角色名
- 配置类里面的hasRole必须为角色名,不可加ROLE
- access里面hasRole的值可以是角色,也可以加ROLE_角色名
4、记住我
**Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问**
DEFAULT_REMEMBER_ME_NAME = "remember-me";
4.1、前期准备
导入依赖:
<!-- mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql 数据库依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
application.yml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
4.2、修改配置类
注册一个bean
- PersistentTokenRepository
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表 第二次删除
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
注入元素
- 顺序不能乱
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Autowired
private UserDetailServiceImpl userDetailService;
参数配置
//记住我
http.rememberMe()
//.rememberMeParameter("remember-me")
//超期时间 默认两周 秒
.tokenValiditySeconds(60)
//自定义登录逻辑
.userDetailsService(userDetailService)
.tokenRepository(persistentTokenRepository);
5、Thymeleaf中SpringSecurity的使用
Spring Security 可以在一些视图技术中进行控制显示效果。例如: JSP 或 Thymeleaf 。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf 作为视图展示技术。
5.1、前期准备
导入依赖
<!--thymeleaf springsecurity5 依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
获取属性
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
获取属性
根据源码得出下面属性:
- name :登录账号名称
- principal :登录主体,在自定义登录逻辑中是 UserDetails credentials :
- 凭证 authorities :权限和角色
- details :实际上是 WebAuthenticationDetails 的实例。
- 可以获取 remoteAddress (客户端 ip)和 sessionId (当前 sessionId)
demo.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
登录账号:<span sec:authentication="name"></span><br/>
登录账号:<span sec:authentication="principal.username"></span><br/>
凭证:<span sec:authentication="credentials"></span><br/>
权限和角色:<span sec:authentication="authorities"></span><br/>
客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
sessionId:<span sec:authentication="details.sessionId"></span><br/>
</body>
</html>
controller增加跳转
@RequestMapping("/demo")
//@ResponseBody
public String demo(){
return "demo";
}
5.2、权限判断
设置用户角色和权限
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc,/insert,/delete"));
控制页面显示效果
通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abc')">查看</button>
5.3、注销功能:
<!--<a href="/user/logout">注销</a>-->
<a href="/user/logout">注销</a>
<!--初始-->
<a href="/logout">注销</a>
//注销
http
.logout()
//可以自定义路径
//.logoutUrl("/user/logout")
.logoutSuccessUrl("/login.html");
//销毁HttpSession对象
.invalidateHttpSession()
//清除对象认证状态
.clearAuthentication()
//退出成功处理器
.logoutSuccessHandler()
源码:
- LogoutConfigurer
- SecurityContextLogoutHandler
- LogoutSuccessHandler
//清除session
private boolean invalidateHttpSession = true;
//清除认证对象
private boolean clearAuthentication = true;
作用:
- 页面跳转到
/logout
- 销毁
HttpSession
- 清除对象的认证状态
5.4、csrf:
在配置类中一直存在这样一行代码:`http.csrf().disable();`如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护
什么是csrf
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack”或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
Spring Security中的CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf
值为token(token在服务端产生)的内容,如果token和服务端token
匹配成功,则正常访问。
测试:
编写控制器方法
- 跳转到 templates 中 login.html 页面。
@RequestMapping(value = "/showLogin")
public String showLogin() {
return "login";
}
开启请求伪造
- 注释即可
//关闭csrf防护关闭 跨站请求伪造
// http.csrf().disable();
login
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}">
用户名:<input type="text" name="username" /><br/>
密码:<input type="password" name="password" /><br/>
<input type="submit" value="登录" />
</form>
</body>
</html>
6、Oauth2认证
Oauth2简介
- 第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要 遵循一定的接口协议。
- OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以 使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。
- 业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的 时间,因而OAUTH是简易的。
- 互联网很多服务如Open API,很多大公司如Google,Yahoo, Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
- Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth 协议:https://tools.ietf.org/html/rfc6749
6.1、实例分析:
)
- 点击“微信”出现一个二维码,此时用户扫描二维码,开始给网站授权,用户是自己在微信信息的资源拥有者
- 资源拥有者同意给客户端授权
- 资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证,
- 验 证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权, 微信认证服务器会颁发一个授权码,并重定向到网站。
- 客户端获取到授权码,请求认证服务器申请令牌
- 此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
- 认证服务器向客户端响应令牌 —不可见
- 认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。
- 客户端请求资源服务器的资源
- 客户端携带令牌访问资源服务器的资源。
- 网站携带令牌请求访问微信服务器获取用户的基本信息。
- 资源服务器返回受保护资源
- 资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常 要请求认证服务器来校验令牌的合法性。
6.2、Oauth2.0认证流程
引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
6.2.1、角色
客户端
- 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,
- 比如:Android客户端、Web 客户端(浏览器端)、微信客户端等。
资源拥有者
- 通常为用户,也可以是应用程序,即该资源的拥有者。
授权服务器
- (也称认证服务器)用来对资源拥有的身份进行认证、对访问资源进行授权。
- 客户端要想访问资源需要通过认证服务器由资 源拥有者授权后方可访问。
资源服务器
- 存储资源的服务器。
- 比如,网站用户管理服务器存储了网站用户信息,网站相册服务器存储了用户的相 册信息,微信的资源服务存储了微信的用户信息等。
- 客户端最终访问资源服务器获取资源信息。
6.2.2、常用术语
1、客户凭证(client Credentials) :客户端的clientId和密码用于认证客户
2、令牌(tokens) :授权服务器在接收到客户请求后,颁发的访问令牌
3、作用域(scopes) :客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)
6.2.3、优缺点:
优点:
- 更安全,客户端不接触用户密码,服务器端更易集中保护
- 广泛传播并被持续采用
- 短寿命和封装的token
- 资源服务器和授权服务器解耦
- 集中式授权,简化客户端
- HTTP/JSON友好,易于请求和传递token
- 考虑多种客户端架构场景
- 客户可以具有不同的信任级别
缺点:
- 协议框架太宽泛,造成各种实现的兼容性和互操作性差
- 不是一个认证协议,本身并不能告诉你任何用户信息。
6.3、授权模式(常用)
OAuth2 四种授权模型、OAuth 2.0 的四种方式、理解OAuth2
1、授权码模式(Authorization Code)
USER-AGENT:浏览器
2、简化授权模式(Implicit)
- 提前获取访问令牌,但因为在Fragment中无法访问;
- 通过脚本命令生成
3、密码模式(Resource Owner PasswordCredentials)
4、客户端模式(Client Credentials)
5、刷新令牌
访问令牌过期后不用再重新走一遍流程,可通过刷新令牌从授权服务器重新获取访问令牌
6.4、Spring Security Oauth2
6.4.1、授权服务器
- Authorize Endpoint :授权端点,进行授权
- Token Endpoint :令牌端点,经过授权拿到对应的Token
- Introspection Endpoint :校验端点,校验Token的合法性
- Revocation Endpoint :撤销端点,撤销授权
6.4.2、Spring Security Oauth2架构
流程:
- 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被 Oauth2ClientContextFilter捕获并重定向到认证服务器
- 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端
- 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生 成Token并返回给客户端
- 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用 ResourceServerTokenServices进行校验。校验通过可以获取资源。
6.4.3、Spring Security Oauth2
前期准备
创建springboot项目,导入依赖
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
实体类
用户-实现UserDetails
public class User implements UserDetails {
private String username;
private String password;
/**
* 权限
*/
private List<GrantedAuthority> authorities;
public User(String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
//帐户未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账户未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//凭证未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否启用
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
}
业务逻辑
用户详情服务的定义实现
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("root");
return new User("admin",password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
配置
安全配置类
@Configuration
//启用Security,启用网络安全
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码解析器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf防护
http.csrf().disable()
//URL权限配置
.authorizeRequests()
//匹配内容
.antMatchers("/oauth/**","/login/**","logout/**")
//允许所有
.permitAll()
//任何请求,必须放在最后,有严格的执行顺序
.anyRequest()
//表示所匹配的 URL 都需要被认证才能访问
.authenticated()
//可以通过and连接,也可以分开写
.and()
//基于表单提交登录
.formLogin()
.permitAll();
}
}
AuthorizationServiceConfig授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
///内存
clients.inMemory()
//配置client-id
.withClient("admin")
//配置client-secret 密钥
.secret(passwordEncoder.encode("112233"))
//配置token有效期
.accessTokenValiditySeconds(3600)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirectUris,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//配置申请的权限的范围
.scopes("all")
//配置grant_type,表示授权类型
.authorizedGrantTypes("authorization_code");
}
}
ResourceServceConfig资源服务器
@Configuration
@EnableResourceServer
public class ResourceServceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
//所有的请求都要经过验证,资源过滤放行"/user下所有的资源"
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/user/**");
}
}
controller
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 获取当前用户
* @param authentication
* @return
*/
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
//返回主体
return authentication.getPrincipal();
}
}
测试
授权码模式
postman调试
grant_type :授权类型,填写authorization_code,表示授权码模式
code :授权码,就是刚刚获取的授权码,
注意:授权码只使用一次就无效了,需要重新申请。
client_id :客户端标识
redirect_uri :申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
scope :授权范围。
认证失败服务端返回 401 Unauthorized
注意:此时无法请求到令牌,访问服务器会报错
密码模式
7、JWT
7.3、JJWT
- jwt只是一套标准,无法具体应用,所以有很多实现产品
- JJWT是一个提供端到端的JWT创建和验证的Java库。
- 永远免费和开源(Apache License,版本2.0),JJW 很容易使用和理解。
- 它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
- 规范官网:https://jwt.io/
1、生成token
- 创建项目
- 导入依赖
<!--JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- 测试
@Test
void contextLoads() {
//1.创建一个jwtbuilder对象
JwtBuilder builder = Jwts.builder();
//2.声明标识{"jti":"888"}
builder.setId("888");
//3.主体,用户{"sub":"Rose"}
builder.setSubject("Rose");
//4.日期{"ita":"xxxxxx"}
builder.setIssuedAt(new Date());
//5.签名,参数1:算法,参数2:盐
builder.signWith(SignatureAlgorithm.HS256,"xxxx");
//6.获取token
String token = builder.compact();
System.out.println(token);
//7.解密 前两部分 第三部分因为盐 保密
String[] split = token.split("//.");
for (String s : split) {
System.out.println(Base64Codec.BASE64.decodeToString(s));
}
}
2、token验证解析
在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
- 测试
public void testParseToken() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTYyNjYwNjc3Nn0.pPDKl5_f5f9RHAfGqzaAiwGwQdpOUYMjiQ9BCzWlUR0";
//解析token得到负载中声明的对象
Claims claims = Jwts.parser()
//密钥
.setSigningKey("xxxx")
.parseClaimsJws(token)
//得到主体
.getBody();
System.out.println("id:" + claims.getId());
System.out.println("Subject:" + claims.getSubject());
System.out.println("IssuedAt:" + claims.getIssuedAt());
}
3、token过期检验
public void testCreateTokenHasExp() {
//当前时间
long now = System.currentTimeMillis();
//过期时间,一分钟
long exp = now + 60 * 1000;
//创建 JwtBuilder 对象
JwtBuilder jwtBuilder = Jwts.builder()
//声明的标识,{"jti":"8888"}
.setId("8888")
//设置主体,用户{"sub":"Rose"}
.setSubject("Rose")
//创建时间{"ita":"xxxx"}
.setIssuedAt(new Date())
//创建签证参数1:算法,参数2:盐
.signWith(SignatureAlgorithm.HS256,"xxxx")
//设置过期时间
.setExpiration(new Date(exp));
//获取jwt生成的token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("============");
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
//无法解密签名,因为盐 保密
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
public void testParseTokenHasExp() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTYyNjYwNzgyOSwiZXhwIjoxNjI2NjA3ODg5fQ.6URP2EbehTS2qx_t0medQUPQxaxBWnUl0VJxuEdlboQ";
//解析token得到负载中声明的对象
Claims claims = Jwts.parser()
//密钥
.setSigningKey("xxxx")
.parseClaimsJws(token)
//得到主体
.getBody();
System.out.println("id:" + claims.getId());
System.out.println("Subject:" + claims.getSubject());
System.out.println("IssuedAt:" + claims.getIssuedAt());
//格式化时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("签发时间" + simpleDateFormat.format(claims.getIssuedAt()));
System.out.println("过期时间" + simpleDateFormat.format(claims.getExpiration()));
System.out.println("当前时间" + simpleDateFormat.format(new Date()));
}
4、自定义claims
public void testCreateTokenByClaims() {
//创建 JwtBuilder 对象
JwtBuilder jwtBuilder = Jwts.builder()
//声明的标识,{"jti":"8888"}
.setId("8888")
//设置主体,用户{"sub":"Rose"}
.setSubject("Rose")
//创建时间{"ita":"xxxx"}
.setIssuedAt(new Date())
//创建签证,参数1:算法,参数2:盐
.signWith(SignatureAlgorithm.HS256,"xxxx")
//自定义申明
.claim("roles","admin")
.claim("logo","xxx.jpg");
//直接传入map
//.addClaims(map);
//获取jwt生成的token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("============");
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
//无法解密签名
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
public void testParseTokenByClaims() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTYyNjYwODc3MCwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJ4eHguanBnIn0.qZUoGPlEELhhObwMmp9pQ3ojrc_xnNTlkTNiJLXwWrc";
//解析token得到负载中声明的对象
Claims claims = Jwts.parser()
//密钥
.setSigningKey("xxxx")
.parseClaimsJws(token)
//得到主体
.getBody();
System.out.println("id:" + claims.getId());
System.out.println("Subject:" + claims.getSubject());
System.out.println("IssuedAt:" + claims.getIssuedAt());
System.out.println("roles:" + claims.get("roles"));
System.out.println("logo:" + claims.get("logo"));
}