一、Shiro简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果系统默认的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
二、Shiro在Springboot中的使用:
创建一个Springboot项目,pom文件中增加如下依赖:
<properties>
<java.version>1.8</java.version>
</properties>
<!-- springboot统一的依赖管理和插件管理-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.5.3</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!-- druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.3</version>
</dependency>
<!-- thymeleaf的shiro版扩展依赖-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
在页面渲染上,博主采用Thymeleaf模板引擎,可以很好地与Html页面集成
接下来是配置项目文件application.yml:
server:
port: 8085
spring:
thymeleaf:
prefix: classpath:/templates/
check-template-location: true
cache: false
suffix: .html
mode: HTML5
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yfzt?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
web:
resources:
static-locations: classpath:/static/
logging:
level:
com:
gec:
official:
mapper: debug
mybatis:
mapper-locations: classpath:com/gec/official/mapper/*:xml
静态文件放在resources/static文件夹下:
HTML页面放在resources/templates文件夹下:
创建启动类:
package com.gec.official;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author liuxin
* @Date 2021/7/29
* @apiNote
*/
@SpringBootApplication
@MapperScan("com.gec.official.mapper")
public class OfficialApp {
public static void main(String[] args) {
SpringApplication.run(OfficialApp.class,args);
}
}
接下来我们要创建一个配置类,用来管理ShiroFilterFactoryBean、DefaultWebSecurityManager还有Realm等:
/**
* @author liuxin
* @Date 2021/7/31
* @apiNote
*/
@Configuration
public class ShiroConfig {
//shiro方言,加了方言才能让shiro标签在html中生效
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
//1.创建shiroFilter 负责拦截所有请求
@Bean( name="shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给fiter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map<String,String> map=new HashMap<>();
//anon设置为公共资源
map.put("/user/doLogin","anon");
map.put("/user/toRegister","anon");
map.put("/user/doRegister","anon");
map.put("/user/selUser","anon");
map.put("/user/getImage","anon");
//设置放行的静态资源
map.put("/js/**","anon");
map.put("/css/**","anon");
//authc请求这个资源需要认证和授权
map.put("/user/index","authc");
map.put("/official/system","authc");
map.put("/**","authc");
//配置系统受限资源
//配置系统公共资源
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//设置默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/user/toLogin");
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(){
CustomerRealm realm = new CustomerRealm();
//修改凭证校验匹配器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//设置加密算法为MD5
matcher.setHashAlgorithmName("MD5");
//设置散列次数
matcher.setHashIterations(1024);
realm.setCredentialsMatcher(matcher);
//开启缓存管理器
realm.setCacheManager(new RedisCacheManager());
realm.setCachingEnabled(true);//开启全局缓存
realm.setAuthenticationCachingEnabled(true);//开启认证缓存
realm.setAuthenticationCacheName("authenticationCache");
realm.setAuthorizationCachingEnabled(true);//开启授权缓存
realm.setAuthorizationCacheName("authorizationCache");
return realm;
}
}
Realm为自定义的,所以我们要创建一个类继承自AuthorizingRealm:
/**
* @author liuxin
* @Date 2021/7/31
* @apiNote
*/
//自定义realm
public class CustomerRealm extends AuthorizingRealm {
//授权:作用是将数据库查到的用户赋予相应的角色和权限,即授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取身份信息
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("调用授权验证:"+primaryPrincipal);
//根据主身份信息获取角色 和 权限信息
UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
User user = userService.findRolesByUserName(primaryPrincipal);
System.out.println("user roles= "+user.getRoles());
//授权角色、权限信息
if (!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
System.out.println("------------分割线------------");
//给user授权数据库中查到的role角色
System.out.println("role = "+role.getName());
info.addRole(role.getName());
List<Role> roles = userService.findPermissionsByRoleId(role.getId());
if (!CollectionUtils.isEmpty(roles)){
roles.forEach(role2 -> {
List<Permission> permissions = role2.getPermissions();
for (Permission p:permissions
) {
//给user授权数据库查到的permission权限
System.out.println("permission = "+p.getName());
info.addStringPermission(p.getName());
}
});
}
});
return info;
}
return null;
}
//验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("============");
//身份信息
String principal = (String) token.getPrincipal();
UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
User user = userService.selUserInfo(principal);
if (user!=null){
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), new MyByteSource(user.getSalt()),this.getName());
}
return null;
}
}
在认证方法中我们使用了一个ApplicationContextUtils类,其实它是自定义的一个类,用来创建相应bean的工厂类,例如获取userServiceImpl的bean,和使用@Autowired在realm中定义UserService的作用一样:
/**
* 用于指定名来生产bean的工厂类
* @author liuxin
* @Date 2021/8/5
* @apiNote
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context=applicationContext;
}
//根据bean名字获取工厂中指定bean对象
public static Object getBean(String beanName){
Object bean = context.getBean(beanName);
return bean;
}
}
我们可以往回看ShiroConfig类中的getRealm方法,发现最下面有开启了缓存管理器,这是因为没有缓存管理器之前我们每登录一次或者在登录成功后刷新页面都会重新进入,大大减缓了效率。这时如果有缓存管理器我们认证成功后每次刷新页面都不用重新进入授权方法。
MD5加密和Hash散列都要先在getRealm方法中的凭证校验管理器matcher声明出来,然后在UserServiceImpl的注册方法register中生成随机盐,密码也要用MD5加密生成
@Override
public void register(User user) {
//生成随机盐
String salt = SaltUtil.getSalt(8);
System.out.println("salt="+salt);
//将salt保存到数据库
user.setSalt(salt);
//明文密码——》md5+salt+hash散列
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
//将加密后的密码赋予给user
user.setPassword(md5Hash.toHex());
//添加用户
userMapper.save(user);
//添加用户后,赋予普通user角色
userMapper.insertUserRole(user.getId());
}
这里要注意的一个点:在添加用户信息后,还要在t_user_role添加相应的role角色,否则会在下一次登录中报错。
自定义一个用于slat的序列化工具类:
/**
* 用于slat的序列化工具类
* @author liuxin
* @Date 2021/8/5
* @apiNote
*/
public class MyByteSource implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MyByteSource() {
}
public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MyByteSource(File file) {
this.bytes = (new BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
生成随机盐的工具类:
public class SaltUtil {
/**
* 生成salt的静态方法
* @return
*/
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*.".toCharArray();
StringBuilder sb=new StringBuilder();
for (int i=0;i<n;i++){
char achar = chars[new Random().nextInt(chars.length)];
sb.append(achar);
}
return sb.toString();
}
public static void main(String[] args) {
String salt = getSalt(8);
System.out.println(salt);
}
}
生成验证码的工具类:
/**
* 验证码工具类
* @author liuxin
* @Date 2021/8/5
* @apiNote
*/
public class VerifyCodeUtils{
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系统默认字符源生成验证码
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
}
/**
* 生成随机验证码文件,并返回验证码值
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
/**
* 输出随机验证码图片流,并返回验证码值
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
/**
* 生成指定验证码图像文件
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException{
if(outputFile == null){
return;
}
File dir = outputFile.getParentFile();
if(!dir.exists()){
dir.mkdirs();
}
try{
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch(IOException e){
throw e;
}
}
/**
* 输出指定验证码图片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h-4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
}
验证码工具类:
/**
* 验证码工具类
* @author liuxin
* @Date 2021/8/5
* @apiNote
*/
public class VerifyCodeUtils{
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系统默认字符源生成验证码
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
}
/**
* 生成随机验证码文件,并返回验证码值
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
/**
* 输出随机验证码图片流,并返回验证码值
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
/**
* 生成指定验证码图像文件
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException{
if(outputFile == null){
return;
}
File dir = outputFile.getParentFile();
if(!dir.exists()){
dir.mkdirs();
}
try{
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch(IOException e){
throw e;
}
}
/**
* 输出指定验证码图片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h-4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public static void main(String[] args) throws IOException {
//获取验证码
String s = generateVerifyCode(4);
//将验证码放入图片中
outputImage(260,60,new File("/Users/chenyannan/Desktop/安工资料/aa.jpg"),s);
System.out.println(s);
}
}
还有Redis缓存管理器类:
/**
* 缓存管理器类:redis
* @author liuxin
* @Date 2021/8/6
* @apiNote
*/
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
return new RedisCache(cacheName);
}
}
缓存类RedisCache:
/**
* Redis的自定义Cache
* @author liuxin
* @Date 2021/8/4
* @apiNote
*/
public class RedisCache<K,V> implements Cache<K,V>{
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public V get(K k) throws CacheException {
System.out.println("get key:"+k);
return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
System.out.println("put key: "+k);
System.out.println("put value:"+v);
getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
System.out.println("=============remove=============");
return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
System.out.println("=============clear==============");
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
控制层:
package com.gec.official.controller;
import com.gec.official.entity.Result;
import com.gec.official.entity.User;
import com.gec.official.entity.User2;
import com.gec.official.service.UserService;
import com.gec.official.utils.VerifyCodeUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Enumeration;
/**
* SpringMVC的视图控制层
*
* @author liuxin
* @Date 2021/7/29
* @apiNote
*/
@Controller
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService userService;
//跳转登录页面
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
//跳转主页面
@RequestMapping("/index")
public String main() {
return "index";
}
//跳转注册页面
@RequestMapping("/toRegister")
public String toRegister() {
return "register";
}
//用户注销
@RequestMapping("/logout")
public String logout(HttpServletRequest request) {
//注销退出时清空缓存
Enumeration em = request.getSession().getAttributeNames();
while (em.hasMoreElements()) {
request.getSession().removeAttribute(em.nextElement().toString());
}
return "redirect:/user/toLogin";
}
//生成随机验证码
@RequestMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
//4位随机验证码
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
System.out.println("验证码为:" + verifyCode);
//生成的验证码立刻存入session
session.setAttribute("verifyCode", verifyCode);
//验证码存入图片
ServletOutputStream so = response.getOutputStream();
response.setContentType("image/png");
VerifyCodeUtils.outputImage(200, 40, so, verifyCode);
}
//登录按钮
@ResponseBody
@RequestMapping("/doLogin")
public Result doLogin(String username, String password, String verifyCode, HttpSession session) {
String sessionVerifyCode = (String) session.getAttribute("verifyCode");
try{
if (sessionVerifyCode.equalsIgnoreCase(verifyCode)){
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username,password));
session.setAttribute("username", username);
return new Result(200, "登录成功");
}else {
return new Result(400,"验证码错误!");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("账号错误");
return new Result(400, "账号错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
return new Result(400, "密码错误");
}catch (Exception e){
e.printStackTrace();
return new Result(404,e.getMessage());
}
}
// //登录按钮(ant.design)
// @ResponseBody
// @RequestMapping("/doLogin")
// public Result doLogin(@RequestBody User2 user2, HttpSession session) {
// try{
// Subject subject = SecurityUtils.getSubject();
// subject.login(new UsernamePasswordToken(user2.getUsername(),user2.getPassword()));
// session.setAttribute("username", user2.getUsername());
// return new Result(200, "登录成功");
// } catch (UnknownAccountException e) {
// e.printStackTrace();
// System.out.println("账号错误");
// return new Result(400, "账号错误");
// } catch (IncorrectCredentialsException e) {
// e.printStackTrace();
// System.out.println("密码错误");
// return new Result(400, "密码错误");
// }catch (Exception e){
// e.printStackTrace();
// return new Result(404,e.getMessage());
// }
// }
//注册按钮
@ResponseBody
@RequestMapping("/doRegister")
public Result doRegister(User user) {
try {
userService.register(user);
return new Result(200,"注册成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(400,"注册失败");
}
}
@ResponseBody
@RequestMapping("/selUser")
public Result selUser(String username) {
User user = userService.selUserInfo(username);
if (user != null) {
return new Result(400, "账号已存在");
} else {
return new Result(200, "账号不存在");
}
}
}
逻辑层:
package com.gec.official.service.impl;
import com.gec.official.entity.Role;
import com.gec.official.entity.User;
import com.gec.official.mapper.UserMapper;
import com.gec.official.service.UserService;
import com.gec.official.utils.SaltUtil;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
/**
* @author liuxin
* @Date 2021/7/29
* @apiNote
*/
@Service("userServiceImpl")
@Transactional
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
//根据用户名登陆
@Override
public User selUserInfo(String uname) {
return userMapper.selUserInfo(uname);
}
@Override
public void register(User user) {
//生成随机盐
String salt = SaltUtil.getSalt(8);
System.out.println("salt="+salt);
//将salt保存到数据库
user.setSalt(salt);
//明文密码——》md5+salt+hash散列
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
//将加密后的密码赋予给user
user.setPassword(md5Hash.toHex());
//添加用户
userMapper.save(user);
//添加用户后,赋予普通user角色
userMapper.insertUserRole(user.getId());
}
@Override
public User findRolesByUserName(String username){
return userMapper.findRolesByUserName(username);
}
@Override
public List<Role> findPermissionsByRoleId(Integer id) {
return userMapper.findPermissionsByRoleId(id);
}
}
持久层:
package com.gec.official.mapper;
import com.gec.official.entity.Role;
import com.gec.official.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper {
//用户注册
void save(@Param("user") User user);
//登录查询
@Select("select * from t_user where username = #{username}")
User selUserInfo(@Param("username") String username);
//根据角色id查询权限集合
List<Role> findPermissionsByRoleId(Integer id);
User findRolesByUserName(String username);
@Insert("insert into t_user_role(userid,roleid) VALUES(#{id},2)")
void insertUserRole(Integer id);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gec.official.mapper.UserMapper">
<insert id="save" keyColumn="id" useGeneratedKeys="true" keyProperty="id" parameterType="com.gec.official.entity.User">
insert into t_user(username,password,salt,phone_number,address,e_mail)
VALUES (
#{user.username},
#{user.password},
#{user.salt},
#{user.phoneNumber},
#{user.address},
#{user.eMail}
);
</insert>
<resultMap id="userMap" type="com.gec.official.entity.User">
<id column="uid" property="id"></id>
<result column="uname" property="username"></result>
<result column="upassword" property="password"></result>
<result column="salt" property="salt"></result>
<result column="phone_number" property="phoneNumber"></result>
<result column="address" property="address"></result>
<result column="e_mail" property="eMail"></result>
<!-- 角色信息-->
<collection property="roles" javaType="list" ofType="com.gec.official.entity.Role">
<id column="rid" property="id"></id>
<result column="rname" property="name"></result>
</collection>
</resultMap>
<select id="findRolesByUserName" parameterType="java.lang.String"
resultMap="userMap">
select u.id uid,u.username uname,u.password upassword,u.salt,u.phone_number,u.address,u.e_mail,r.id rid,r.name rname
from t_user u
INNER JOIN t_user_role ur on u.id = ur.userid
INNER JOIN t_role r on ur.roleid = r.id
where username=#{username};
</select>
<resultMap id="roleMap" type="com.gec.official.entity.Role">
<id column="id" property="id"></id>
<result column="pname" property="name"></result>
<!-- 权限信息-->
<collection property="permissions" javaType="list" ofType="com.gec.official.entity.Permission">
<id column="pid" property="id"></id>
<result column="pname" property="name"></result>
<result column="url" property="url"></result>
</collection>
</resultMap>
<select id="findPermissionsByRoleId" parameterType="java.lang.Integer" resultMap="roleMap">
SELECT r.id, r.name, p.id pid,p.name pname,p.url from t_role r
left JOIN t_role_permission rp on r.id = rp.roleid
left JOIN t_permission p on rp.permid = p.id
where r.id=#{id};
</select>
</mapper>
登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/login.css">
<title>登录页</title>
<!--引入JQuery文件-->
<script type="text/javascript" src="/js/jquery-3.3.1.js"></script>
<!--声明js代码域-->
<script type="text/javascript">
// 登录按钮
function login() {
var username = $("#u1").val();
var password = $("#p1").val();
var verifyCode = $("#c1").val();
console.log(username,password);
$.ajax({
url:'/user/doLogin',
type:'post',
data:{'username':username,'password':password,'verifyCode':verifyCode},
dataType:'json',
success:function(data){
if(data.code==200){
alert(data.msg);
location.href='/user/index';
}else{
document.getElementById("msg").innerText=data.msg;
}
},
error:function(){
console.log('请求出错!');
}
})
}
// 点击验证码图片重新刷新
function flush(e) {
const source = e.src ; // 获得原来的 src 中的内容
var index = source.indexOf( "?" ) ; // 从 source 中寻找 ? 第一次出现的位置 (如果不存在则返回 -1 )
if( index > -1 ) { // 如果找到了 ? 就进入内部
var s = source.substring( 0 , index ) ; // 从 source 中截取 index 之前的内容 ( index 以及 index 之后的内容都被舍弃 )
var date = new Date(); // 创建一个 Date 对象的 一个 实例
var time = date.getTime() ; // 从 新创建的 Date 对象的实例中获得该时间对应毫秒值
e.src = s + "?time=" + time ; // 将 加了 尾巴 的 地址 重新放入到 src 上
} else {
var date = new Date();
e.src = source + "?time=" + date.getTime();
}
}
</script>
</head>
<body>
<section>
<!-- 背景颜色 -->
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<div class="box">
<!-- 背景圆 -->
<div class="circle" style="--x:0"></div>
<div class="circle" style="--x:1"></div>
<div class="circle" style="--x:2"></div>
<div class="circle" style="--x:3"></div>
<div class="circle" style="--x:4"></div>
<!-- 登录框 -->
<div class="container">
<div class="form">
<h2>元沣智能科技有限公司</h2>
<form>
<div class="inputBox">
<input type="text" id="u1" name="username" placeholder="用戶名">
</div>
<div class="inputBox">
<input type="password" id="p1" name="password" placeholder="密码">
</div>
<div class="inputBox">
<input type="text" id="c1" name="verifyCode" placeholder="请输入下图中的验证码"><br><br>
<img src="/user/getImage" alt="" onclick="flush(this)">
</div>
<div class="inputBox">
<input id="btn" type="button" value="登录" onclick="login()">
<span style="color: red;font-size: 18px;font-weight: bold" id="msg"></span>
</div>
</form>
<!-- <span style="color: red;font-size: 18px;font-weight: bold" th:text="${errorMsg}"></span>-->
<p class="forget">忘记密码?<a href="#">
点击这里
</a>
</p>
<p class="forget">没有账户?<a href="/user/toRegister">
注册
</a>
</p>
</div>
</div>
</div>
</section>
</body>
</html>
主页
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>元沣智能科技有限公司官网</title>
<link href="/css/page.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
<link href="/css/yfzn.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-stick-dark navbar-dark" data-navbar="sticky">
<div class="container">
<div class="navbar-left">
<button class="navbar-toggler" type="button">☰</button>
<a class="navbar-brand" href="">
<img class="logo-dark" src="/img/yfzn.jpg" alt="logo">
</a>
</div>
<section class="navbar-mobile">
<ul class="nav-navbar nav ml-auto">
<li class="nav-item">
<a class="nav-link active" href="/">首页</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">社区</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">物联网</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">防疫系统</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">智慧城市</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">智感小区</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">元沣智瞳</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">海心桥</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/">平安技术</a>
</li>
<li class="nav-item" shiro:hasRole="admin">
<a class="nav-link active" th:href="@{/official/system}">官网系统</a>
</li>
</ul>
<div class="nav-download d-inline-flex">
<a class="nav-link" href="#">
<shiro:principal></shiro:principal>
</a>
<a class="btn" href="/user/logout">注销</a>
</div>
</section>
</div>
</nav>
<header class="header h-fullscreen" style="background-image: linear-gradient(150deg, #fdfbfb 0%, #eee 100%);">
<div class="container">
<div class="row h-full align-items-center text-center text-lg-left">
<div class=" col-12 col-lg-4 ">
<h2>元沣,让智能贴近生活</h2> <br>
<p class="lead mx-auto">我们是一家物联网公司,经营范围十分广泛 </p> <br>
<p class="lead mx-auto">自2016年起,公司发展十分迅速,技术成长日渐成熟。现已有多个合作伙伴,包括政府部门如广州公安、广州体育组委会等,明星企业如华为、海康威视、天龙等 </p>
<br>
<a class="btn btn-lg" href="/download">体验由此开始</a>
<p class="pt-8"><small>不管世界的互联网发生什么样的变化,只要能进一步方便客户的生活体验,我们就会无时无刻守护它。</small></p>
</div>
<div class="col-12 col-lg-6 offset-lg-1">
<img src="/img/yfzn2.png" data-aos="slide-left" class="aos-init aos-animate" alt="logo">
</div>
</div>
</div>
</header>
</body>
</html>
注册页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/login.css">
<!--引入JQuery文件-->
<script type="text/javascript" src="/js/jquery-3.3.1.js"></script>
<!--声明js代码域-->
<script type="text/javascript">
$(document).ready(function(){
//账号输入框失去焦点方法
$("#u1").blur(function () {
var username = $("#u1").val();
console.log(username);
$.ajax({
url: '/user/selUser',
type: 'post',
data: {'username':username},
dataType: 'json',
success: function (data) {
if (data.code == 400) {
document.getElementById("msg").innerText = data.msg;
}else {
document.getElementById("msg").innerText = null;
}
},
error: function () {
console.log('请求出错!');
}
})
})
});
function register() {
var username = $("#u1").val();
var password = $("#p1").val();
var address = $("#a1").val();
var phoneNumber = $("#pn1").val();
var eMail = $("#e1").val();
console.log(username,password);
$.ajax({
url:'/user/doRegister',
type:'post',
data:{'username':username,'password':password,'address':address,'phoneNumber':phoneNumber,'eMail':eMail},
dataType:'json',
success:function(data){
if(data.code==200){
alert(data.msg);
//返回登录页面
location.href='/user/toLogin';
}else{
document.getElementById("msg").innerText=data.msg;
}
},
error:function(){
console.log('请求出错!');
}
})
}
</script>
<title>注册页</title>
</head>
<body>
<section>
<!-- 背景颜色 -->
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<div class="box">
<!-- 背景圆 -->
<div class="circle" style="--x:0"></div>
<div class="circle" style="--x:1"></div>
<div class="circle" style="--x:2"></div>
<div class="circle" style="--x:3"></div>
<div class="circle" style="--x:4"></div>
<!-- 登录框 -->
<div class="container">
<div class="form">
<h2>元沣智能科技有限公司</h2>
<form action="/user/doRegister" method="post">
<div class="inputBox">
<input type="text" id="u1" name="username" placeholder="用戶名">
</div>
<div class="inputBox">
<input type="password" id="p1" name="password" placeholder="密码">
</div>
<div class="inputBox">
<input type="text" id="pn1" name="phoneNumber" placeholder="电话号码">
</div>
<div class="inputBox">
<input type="text" id="a1" name="address" placeholder="住址">
</div>
<div class="inputBox">
<input type="text" id="e1" name="eMail" placeholder="邮箱">
</div>
<div class="inputBox">
<input id="btn" type="button" value="注册" onclick="register()">
<span style="color: red;font-size: 18px;font-weight: bold" id="msg"></span>
</div>
</form>
<p class="forget">已有账号?<a href="/user/toLogin">
登录
</a>
</p>
</div>
</div>
</div>
</section>
</body>
</html>