“认证”(Authentication),是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统)。
“授权”(Authorization)指确定一个主体是否允许在你的应用程序执行一个动作的过程。为了抵达需要授权的店,主体的身份已经有认证过程建立。
依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
配置web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Archetype Created Web Application</display-name>
<!--Spring Security过滤器链,注意过滤器名称必须叫springSecurityFilterChain-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
创建SpringSecurity配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--直接释放无需经过SpringSecurity过滤器的静态资源-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/favicon.ico" security="none"/>
<!--设置可以用spring的el表达式配置Spring Security并自动生成对应配置组件(过滤器)-->
<security:http auto-config="true" use-expressions="true">
<!--指定login.jsp页面可以被匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
<!--指定自定义的认证页面-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"/>
<!--指定退出登录后跳转的页面-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
</security:http>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user"
authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin"
authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
引入SpringSecurity配置文件到application中
<!--引入SpringSecurity主配置文件-->
<import resource="classpath:spring-security.xml"/>
配置中启动csrf
表单提交时使用隐藏域提交token,这里使用了thymeleaf模板,注意登录,注销都需要提交隐藏域,而且必须是post提交
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
1.引入SpirngSecurity
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.编写SpringSecurity的配置类
继承WebSecurityConfigurerAdapter
@EnableWebSecurity
:配置类带上这个注解,这个注解已经使用了@Configuration
,所以不需要再加
控制请求的访问权限
ps:如遇以下错误信息,是模板与springboot冲突的问题,修改一下版本
An attempt was made to call the method org.thymeleaf.spring5.SpringTemplateEngine.setRenderHiddenMarkersBeforeCheckboxes(Z)V but it does not exist. Its class, org.thymeleaf.spring5.SpringTemplateEngine, is available from the following locations:
jar:file:/E:/springboot/repository/org/thymeleaf/thymeleaf-spring5/3.0.9.RELEASE/thymeleaf-spring5-3.0.9.RELEASE.jar!/org/thymeleaf/spring5/SpringTemplateEngine.class
It was loaded from the following location:
file:/E:/springboot/repository/org/thymeleaf/thymeleaf-spring5/3.0.9.RELEASE/thymeleaf-spring5-3.0.9.RELEASE.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of org.thymeleaf.spring5.SpringTemplateEngine
<thymeleaf-spring5.version>3.0.9.RELEASE</thymeleaf-spring5.version>
<!-- 布局功能的支持程序 thymeleaf3主程序 layout2以上版本 -->
<!-- thymeleaf2 layout1-->
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
2.1.x的springboot版本是security5
2.0.x的是security4
spring securuty5 默认使用密码加密,在配置类中创建bean交由springboot管理即可,后续要使用时再注入
@Bean
public PasswordEncoder createPwdEncoder() {
return new BCryptPasswordEncoder();
}
命名空间使用略有不同,pom文件也要修改一下导入的依赖
<!-- 2.1.x的springboot版本是security5 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- 2.0.x的是security4 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security4">
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1 align="center">欢迎光临武林秘籍管理系统</h1>
<div sec:authorize="!isAuthenticated()">
<h2 align="center">游客您好,如果想查看武林秘籍 <a th:href="@{/userlogin}">请登录</a></h2>
</div>
<div sec:authorize="isAuthenticated()">
<h2><span sec:authentication="name"></span>,您好,您的角色有:<span sec:authentication="principal.authorities"></span></h2>
<form th:action="@{/logout}" method="post">
<input type="submit" value="注销">
</form>
</div>
<hr>
<div sec:authorize="hasRole('VIP1')">
<h3>普通武功秘籍</h3>
<ul>
<li><a th:href="@{/level1/1}">罗汉拳</a></li>
<li><a th:href="@{/level1/2}">武当长拳</a></li>
<li><a th:href="@{/level1/3}">全真剑法</a></li>
</ul>
</div>
<div sec:authorize="hasRole('VIP2')">
<h3>高级武功秘籍</h3>
<ul>
<li><a th:href="@{/level2/1}">太极拳</a></li>
<li><a th:href="@{/level2/2}">七伤拳</a></li>
<li><a th:href="@{/level2/3}">梯云纵</a></li>
</ul>
</div>
<div sec:authorize="hasRole('VIP3')">
<h3>绝世武功秘籍</h3>
<ul>
<li><a th:href="@{/level3/1}">葵花宝典</a></li>
<li><a th:href="@{/level3/2}">龟派气功</a></li>
<li><a th:href="@{/level3/3}">独孤九剑</a></li>
</ul>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1 align="center">欢迎登陆武林秘籍管理系统</h1>
<hr>
<div align="center">
<form th:action="@{/userlogin}" method="post">
用户名:<input name="user"/><br>
密码:<input name="pwd"><br/>
<input type="checkbox" name="remember">记住我
<input type="submit" value="登陆">
</form>
</div>
</body>
</html>
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
//定制请求的授权规则
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("VIP1")
.antMatchers("/level2/**").hasRole("VIP2")
.antMatchers("/level3/**").hasRole("VIP3");
//开启自动配置的登录功能,如果没有登录,会来到登录页面
http.formLogin().usernameParameter("user").passwordParameter("pwd").loginPage("/userlogin"); //.loginPage("/userlogin")表示自定义登录页面,不指定就回到默认的
//1./login来到登录页
//2.重定向到/login?error表示登录失败
//3.还有更多详细规定
//4.默认post形式的/login代表处理登录
//5.一旦定制loginPage,那么loginPage的post请求就是登录
//开启自动配置的注销功能
http.logout().logoutSuccessUrl("/"); //.logoutSuccessUrl("/")表示注销成功返回到首页
//1.访问/logout 表示用户注销,清空session
//2.注销成功会返回/login?logout
//开启记住我功能
http.rememberMe().rememberMeParameter("remember");
//登录成功后将cookie发给浏览器保存,以后访问页面会带上这个cookie,只要通过检查就可以免登录
//点击注销会删除cookie
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("wu").password(new BCryptPasswordEncoder().encode("458974")).roles("VIP1","VIP2")
.and()
.withUser("ruo").password("458974").roles("VIP3","VIP2");
}
}
表单中新增复选框,name值为remember-me
<input type="checkbox" name="remember-me" title="记住我" checked>
SpringSecurity配置中开启remember-me功能,设置过期时间和UserDetailService
.and()
.rememberMe().tokenValiditySeconds(86400)
.userDetailsService(myUserDetailService);
这样之后还是存在安全问题,别人可以拿着cookie来进行访问,我们需要持久化cookie
创建持久化表,这是SpringSecurity官方提供的表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
一共有三种注解支持,ka
xml形式
<!--
开启权限控制注解支持
jsr250-annotations="enabled"表示支持jsr250-api的注解,需要jsr250-api的jar包
pre-post-annotations="enabled"表示支持spring表达式注解
secured-annotations="enabled"这才是SpringSecurity提供的注解
-->
<security:global-method-security jsr250-annotations="enabled"
pre-post-annotations="enabled"
secured-annotations="enabled"/>
springboot配置类形式,在SpringSecurity配置类上添加注解
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true) //开权限方法权限注解支持
在controller或者service上开启权限控制
//@PreAuthorize("hasRole('ROLE_MANAGER')")
@Secured({"ROLE_MANAGER","ROLE_USER"})
@RolesAllowed({"ROLE_USER","ROLE_MANAGER"})
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Long gmtCreate;
private Long gmtModified;
private String avatarUrl;
private Integer vipLevel;
private String vipName;
private Boolean status;
private String name;
private Integer sex;
private String description;
private List<Role> roleList = new ArrayList();
@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 Collection<? extends GrantedAuthority> getAuthorities() {
return roleList;
}
}
@Data
public class Role implements GrantedAuthority {
private Integer id;
private String name;
private String description;
@Override
public String getAuthority() {
return name;
}
}
重新写loadUserByUsername方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserExample example = new UserExample();
example.createCriteria().andUsernameEqualTo(username);
com.wu.manager.pojo.User user = userMapper.selectByExample(example).get(0);
user.getVipLevel();
UserGrade userGrade = userGradeMapper.selectByPrimaryKey(user.getVipLevel());
user.setVipName(userGrade.getGradeName());
if (user == null) return null;
List<Role> authorities = authorities(user.getId());
user.setRoleList(authorities);
return user;
}
//给当前用户指定角色
private List<Role> authorities(Integer id) {
List<Role> authorities = new ArrayList<>();
UserRoleExample userRoleExample = new UserRoleExample();
userRoleExample.createCriteria().andUserIdEqualTo(id);
List<UserRole> userRoleList = userRoleMapper.selectByExample(userRoleExample);
for (UserRole userRole : userRoleList) {
Role role = roleMapper.selectByPrimaryKey(userRole.getRoleId());
authorities.add(role);
}
return authorities;
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true) //开权限方法权限注解支持
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder createPwdEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
persistentTokenRepository.setDataSource(dataSource);
return persistentTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll() //对登录请求放行
.antMatchers("/res/**").permitAll()
.antMatchers("/index/**").hasAnyAuthority("VIP")
.antMatchers("/**") //拦截根目录以及根目录下的子目录
.fullyAuthenticated() //对所有的资源进行请求拦截
.and()
.formLogin() //以表单验证的方式对所有的拦截资源进行认证
.loginPage("/login") //自定义登录页面
// .successForwardUrl("/") //登录成功后的跳转页面
.successHandler(new MyAuthenticationSuccessHandler()) //登录成功返回json信息
.failureHandler(new MyAuthenticationFailureHandler()) //登录失败返回json信息
.and()
.rememberMe().tokenValiditySeconds(86400)
.and()
.logout().deleteCookies("remember-me")
.and().headers().frameOptions().sameOrigin()
.and().sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
// .csrf().disable() //关闭跨域访问
;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}
登陆成功返回json
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(JsonUtils.objectToJson(LayUIResult.build(200,"登陆成功")));
out.flush();
out.close();
}
}
登陆失败返回json
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(JsonUtils.objectToJson(LayUIResult.build(400,"登录失败")));
out.flush();
out.close();
}
}
@RequestMapping("/login")
public String login() {
return "/page/login/login";
}
springboot配置类形式,在SpringSecurity配置类上添加注解
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true) //开权限方法权限注解支持
在controller或者service上开启权限控制
//@PreAuthorize("hasRole('ROLE_MANAGER')")
@Secured({"ROLE_MANAGER","ROLE_USER"})
@RolesAllowed({"ROLE_USER","ROLE_MANAGER"})
只需要开启一个即可,可以实现对controller等的权限控制
跨域访问保护默认是开启的,这时你的登陆、退出必须为POST方式,以及增删改必须携带CSRF令牌
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
$.ajax({
type: "POST",
url: "/login",
data: $('#loginForm').serialize(),
dataType: "json",
success: function (loginCallback) {
if (loginCallback.code == '200') {
layer.msg("登录成功", {
icon: 6,
time: 1000 //2秒关闭(如果不配置,默认是3秒)
}, function () { //弹框后的操作
window.location.href = "/";
});
} else {
layer.msg("用户名或密码错误,请重新输入", {
icon: 2,
time: 2000 //2秒关闭(如果不配置,默认是3秒)
});
}
},
error: function (jqXHR) {
layer.alert("发生错误:" + jqXHR.status, {
title: 'Error'
});
}
});
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}">
//ajax请求时都带上csrf信息
$(function () {
var token = $("meta[name='_csrf']").attr("content")
var header = $("meta[name='_csrf_header']").attr("content")
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(header,token)
})
})